mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-26 00:23:55 -07:00
Hover popup.
Took 36 minutes Took 1 minute
This commit is contained in:
parent
5c021f0aad
commit
8007d40a90
7 changed files with 1126 additions and 0 deletions
|
|
@ -354,6 +354,8 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||||
src/interface/widgets/utility/compact_push_button.cpp
|
src/interface/widgets/utility/compact_push_button.cpp
|
||||||
src/interface/widgets/utility/compact_push_button.h
|
src/interface/widgets/utility/compact_push_button.h
|
||||||
|
src/interface/widgets/server/user/user_info_popup.cpp
|
||||||
|
src/interface/widgets/server/user/user_info_popup.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(sounds)
|
add_subdirectory(sounds)
|
||||||
|
|
|
||||||
|
|
@ -537,3 +537,113 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
||||||
|
|
||||||
delete menu;
|
delete menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execChat(const QString &userName)
|
||||||
|
{
|
||||||
|
emit openMessageDialog(userName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execDetails(const QString &userName)
|
||||||
|
{
|
||||||
|
auto *w = new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
|
||||||
|
Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint);
|
||||||
|
w->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
w->updateInfo(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execShowGames(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetGamesOfUser cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::gamesOfUserReceived);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execAddToBuddy(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_AddToList cmd;
|
||||||
|
cmd.set_list("buddy");
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
client->sendCommand(client->prepareSessionCommand(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execRemoveFromBuddy(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_RemoveFromList cmd;
|
||||||
|
cmd.set_list("buddy");
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
client->sendCommand(client->prepareSessionCommand(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execAddToIgnore(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_AddToList cmd;
|
||||||
|
cmd.set_list("ignore");
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
client->sendCommand(client->prepareSessionCommand(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execRemoveFromIgnore(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_RemoveFromList cmd;
|
||||||
|
cmd.set_list("ignore");
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
client->sendCommand(client->prepareSessionCommand(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execBan(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetUserInfo cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUser_processUserInfoResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execWarn(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetUserInfo cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processUserInfoResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execBanHistory(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetBanHistory cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUserHistory_processResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execWarnHistory(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetWarnHistory cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUserHistory_processResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execAdminNotes(const QString &userName)
|
||||||
|
{
|
||||||
|
Command_GetAdminNotes cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
auto *pend = client->prepareModeratorCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::getAdminNotes_processResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge)
|
||||||
|
{
|
||||||
|
Command_AdjustMod cmd;
|
||||||
|
cmd.set_user_name(userName.toStdString());
|
||||||
|
cmd.set_should_be_mod(shouldBeMod);
|
||||||
|
cmd.set_should_be_judge(shouldBeJudge);
|
||||||
|
PendingCommand *pend = client->prepareAdminCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,27 @@ public:
|
||||||
int playerId,
|
int playerId,
|
||||||
const QString &deckHash,
|
const QString &deckHash,
|
||||||
ChatView *chatView = nullptr);
|
ChatView *chatView = nullptr);
|
||||||
|
|
||||||
|
const UserListProxy *getUserListProxy() const
|
||||||
|
{
|
||||||
|
return userListProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual action entry points — used by UserInfoPopup to trigger
|
||||||
|
// actions without re-running the full context menu flow.
|
||||||
|
void execChat(const QString &userName);
|
||||||
|
void execDetails(const QString &userName);
|
||||||
|
void execShowGames(const QString &userName);
|
||||||
|
void execAddToBuddy(const QString &userName);
|
||||||
|
void execRemoveFromBuddy(const QString &userName);
|
||||||
|
void execAddToIgnore(const QString &userName);
|
||||||
|
void execRemoveFromIgnore(const QString &userName);
|
||||||
|
void execBan(const QString &userName);
|
||||||
|
void execWarn(const QString &userName);
|
||||||
|
void execBanHistory(const QString &userName);
|
||||||
|
void execWarnHistory(const QString &userName);
|
||||||
|
void execAdminNotes(const QString &userName);
|
||||||
|
void execAdjustMod(const QString &userName, bool shouldBeMod, bool shouldBeJudge);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
599
cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
Normal file
599
cockatrice/src/interface/widgets/server/user/user_info_popup.cpp
Normal file
|
|
@ -0,0 +1,599 @@
|
||||||
|
#include "user_info_popup.h"
|
||||||
|
|
||||||
|
#include "../../interface/pixel_map_generator.h"
|
||||||
|
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||||
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QStandardItem>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
|
||||||
|
// ── Compact game row delegate ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class PopupGameDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using QStyledItemDelegate::QStyledItemDelegate;
|
||||||
|
|
||||||
|
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override
|
||||||
|
{
|
||||||
|
return QSize(0, 38);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override
|
||||||
|
{
|
||||||
|
const QVariant var = index.data(PopupRoles::GameData);
|
||||||
|
if (!var.isValid()) {
|
||||||
|
QStyledItemDelegate::paint(p, option, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->save();
|
||||||
|
p->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
|
||||||
|
|
||||||
|
const QRect rect = option.rect;
|
||||||
|
const ServerInfo_Game game = var.value<ServerInfo_Game>();
|
||||||
|
const bool selected = option.state & QStyle::State_Selected;
|
||||||
|
|
||||||
|
p->fillRect(rect, selected ? QColor(35, 45, 62) : QColor(14, 18, 26));
|
||||||
|
|
||||||
|
// State colour dot
|
||||||
|
const QColor dot = game.started() ? QColor(239, 68, 68)
|
||||||
|
: (game.player_count() >= game.max_players()) ? QColor(249, 115, 22)
|
||||||
|
: game.with_password() ? QColor(59, 130, 246)
|
||||||
|
: QColor(34, 197, 94);
|
||||||
|
p->setPen(Qt::NoPen);
|
||||||
|
p->setBrush(dot);
|
||||||
|
p->drawEllipse(QRectF(rect.left() + 9, rect.top() + (rect.height() - 8) / 2.0, 8, 8));
|
||||||
|
|
||||||
|
// Game title (bold, elided)
|
||||||
|
QFont tf = option.font;
|
||||||
|
tf.setBold(true);
|
||||||
|
p->setFont(tf);
|
||||||
|
p->setPen(QColor(205, 215, 230));
|
||||||
|
const int textX = rect.left() + 26;
|
||||||
|
const int countW = 52;
|
||||||
|
const int titleW = rect.width() - textX - countW - 6;
|
||||||
|
p->drawText(QRect(textX, rect.top(), titleW, rect.height()), Qt::AlignVCenter | Qt::AlignLeft,
|
||||||
|
QFontMetrics(tf).elidedText(QString::fromStdString(game.description()), Qt::ElideRight, titleW));
|
||||||
|
|
||||||
|
// Player count
|
||||||
|
const bool full = game.player_count() >= game.max_players();
|
||||||
|
p->setFont(option.font);
|
||||||
|
p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150));
|
||||||
|
p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()),
|
||||||
|
Qt::AlignVCenter | Qt::AlignRight,
|
||||||
|
QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players()));
|
||||||
|
|
||||||
|
// Row separator
|
||||||
|
p->setPen(QColor(24, 32, 44));
|
||||||
|
p->drawLine(rect.bottomLeft(), rect.bottomRight());
|
||||||
|
|
||||||
|
p->restore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── UserInfoHeaderWidget ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
UserInfoHeaderWidget::UserInfoHeaderWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
setFixedHeight(HeaderHeight);
|
||||||
|
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &user,
|
||||||
|
bool online,
|
||||||
|
const QPixmap &avatar,
|
||||||
|
const QPixmap &cardArt,
|
||||||
|
const CardArtParams ¶ms)
|
||||||
|
{
|
||||||
|
m_user = user;
|
||||||
|
m_online = online;
|
||||||
|
m_avatar = avatar;
|
||||||
|
m_cardArt = cardArt;
|
||||||
|
m_params = params;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||||
|
|
||||||
|
const QRect rect = this->rect();
|
||||||
|
const UserLevelFlags level(m_user.user_level());
|
||||||
|
const QString userName = QString::fromStdString(m_user.name());
|
||||||
|
const QString privLevel = QString::fromStdString(m_user.privlevel());
|
||||||
|
|
||||||
|
// Dark base
|
||||||
|
p.fillRect(rect, QColor(14, 18, 26));
|
||||||
|
|
||||||
|
// ── Card art background ───────────────────────────────────────────────────
|
||||||
|
if (!m_cardArt.isNull()) {
|
||||||
|
const int w = rect.width();
|
||||||
|
const int h = rect.height();
|
||||||
|
const int mL = qRound(w * m_params.marginPctL);
|
||||||
|
const int mR = qRound(w * m_params.marginPctR);
|
||||||
|
const int dW = w - mL - mR;
|
||||||
|
|
||||||
|
const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height());
|
||||||
|
const double scale = base * m_params.zoom;
|
||||||
|
const int sW = qRound(m_cardArt.width() * scale);
|
||||||
|
const int sH = qRound(m_cardArt.height() * scale);
|
||||||
|
|
||||||
|
const QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
|
const int srcX = (sW - dW) / 2;
|
||||||
|
const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h));
|
||||||
|
|
||||||
|
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
{
|
||||||
|
QPainter mask(&img);
|
||||||
|
mask.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||||
|
QLinearGradient g(0, 0, img.width(), 0);
|
||||||
|
g.setColorAt(0.00, Qt::transparent);
|
||||||
|
g.setColorAt(0.18, Qt::white);
|
||||||
|
g.setColorAt(0.82, Qt::white);
|
||||||
|
g.setColorAt(1.00, Qt::transparent);
|
||||||
|
mask.fillRect(img.rect(), g);
|
||||||
|
}
|
||||||
|
p.setOpacity(0.48);
|
||||||
|
p.drawImage(mL, 0, img);
|
||||||
|
p.setOpacity(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom gradient overlay so avatar and text are always legible
|
||||||
|
{
|
||||||
|
QLinearGradient ov(0, 0, 0, rect.height());
|
||||||
|
ov.setColorAt(0.0, QColor(14, 18, 26, 0));
|
||||||
|
ov.setColorAt(0.55, QColor(14, 18, 26, 110));
|
||||||
|
ov.setColorAt(1.0, QColor(14, 18, 26, 230));
|
||||||
|
p.fillRect(rect, ov);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Avatar ────────────────────────────────────────────────────────────────
|
||||||
|
const QColor accent = [&]() -> QColor {
|
||||||
|
if (level.testFlag(ServerInfo_User::IsAdmin)) {
|
||||||
|
return QColor(245, 158, 11);
|
||||||
|
}
|
||||||
|
if (level.testFlag(ServerInfo_User::IsModerator)) {
|
||||||
|
return QColor(59, 130, 246);
|
||||||
|
}
|
||||||
|
if (level.testFlag(ServerInfo_User::IsJudge)) {
|
||||||
|
return QColor(168, 85, 247);
|
||||||
|
}
|
||||||
|
return QColor(100, 116, 139);
|
||||||
|
}();
|
||||||
|
|
||||||
|
const int ax = LeftPad;
|
||||||
|
const int ay = rect.height() - AvatarSize - 10;
|
||||||
|
const QRect ar(ax, ay, AvatarSize, AvatarSize);
|
||||||
|
|
||||||
|
QPainterPath clip;
|
||||||
|
clip.addEllipse(ar);
|
||||||
|
p.save();
|
||||||
|
p.setClipPath(clip);
|
||||||
|
|
||||||
|
if (!m_avatar.isNull()) {
|
||||||
|
p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
|
||||||
|
} else {
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(accent.darker(200));
|
||||||
|
p.drawEllipse(ar);
|
||||||
|
const QPixmap pawn =
|
||||||
|
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel);
|
||||||
|
p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn);
|
||||||
|
}
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
// Status ring
|
||||||
|
p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5));
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25));
|
||||||
|
|
||||||
|
// ── Username + badge ──────────────────────────────────────────────────────
|
||||||
|
const int tx = ax + AvatarSize + AvatarToTextGap;
|
||||||
|
const int tw = rect.width() - tx - 8;
|
||||||
|
|
||||||
|
QFont nf = font();
|
||||||
|
nf.setBold(true);
|
||||||
|
nf.setPointSizeF(nf.pointSizeF() * 1.12);
|
||||||
|
p.setFont(nf);
|
||||||
|
p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115));
|
||||||
|
p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft,
|
||||||
|
QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw));
|
||||||
|
|
||||||
|
// Level / priv badge
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
QColor color;
|
||||||
|
} badge;
|
||||||
|
if (level.testFlag(ServerInfo_User::IsAdmin)) {
|
||||||
|
badge = {"ADMIN", QColor(245, 158, 11)};
|
||||||
|
} else if (level.testFlag(ServerInfo_User::IsModerator)) {
|
||||||
|
badge = {"MOD", QColor(59, 130, 246)};
|
||||||
|
} else if (level.testFlag(ServerInfo_User::IsJudge)) {
|
||||||
|
badge = {"JUDGE", QColor(168, 85, 247)};
|
||||||
|
} else if (privLevel == "VIP") {
|
||||||
|
badge = {"VIP", QColor(20, 184, 166)};
|
||||||
|
} else if (privLevel == "DONATOR") {
|
||||||
|
badge = {"DONATOR", QColor(249, 115, 22)};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!badge.text.isEmpty()) {
|
||||||
|
QFont bf = font();
|
||||||
|
bf.setPointSizeF(bf.pointSizeF() * 0.70);
|
||||||
|
bf.setBold(true);
|
||||||
|
p.setFont(bf);
|
||||||
|
const QFontMetrics bfm(bf);
|
||||||
|
const int bw = bfm.horizontalAdvance(badge.text) + 10;
|
||||||
|
const QRect br(tx, ay + AvatarSize / 2 + 6, bw, 15);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(badge.color.darker(160));
|
||||||
|
p.drawRoundedRect(br, 3, 3);
|
||||||
|
p.setPen(badge.color.lighter(150));
|
||||||
|
p.drawText(br, Qt::AlignCenter, badge.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UserInfoPopup ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
UserInfoPopup::UserInfoPopup(TabSupervisor *ts,
|
||||||
|
AbstractClient *client,
|
||||||
|
const QMap<QString, QPixmap> *avatarCache,
|
||||||
|
const QMap<QString, QPixmap> *cardArtCache,
|
||||||
|
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||||
|
QWidget *parent)
|
||||||
|
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), m_ts(ts), m_client(client), m_avatarCache(avatarCache),
|
||||||
|
m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_ShowWithoutActivating);
|
||||||
|
setFixedWidth(PopupWidth);
|
||||||
|
setFrameShape(QFrame::NoFrame);
|
||||||
|
buildUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoPopup::buildUi()
|
||||||
|
{
|
||||||
|
setStyleSheet(QStringLiteral("UserInfoPopup {"
|
||||||
|
" background:#0e1218;"
|
||||||
|
" border:1px solid #1e2838;"
|
||||||
|
" border-radius:8px;"
|
||||||
|
"}"));
|
||||||
|
|
||||||
|
auto *root = new QVBoxLayout(this);
|
||||||
|
root->setContentsMargins(0, 0, 0, 0);
|
||||||
|
root->setSpacing(0);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
m_header = new UserInfoHeaderWidget(this);
|
||||||
|
root->addWidget(m_header);
|
||||||
|
|
||||||
|
// Action area — rebuilt per user
|
||||||
|
m_actionArea = new QWidget(this);
|
||||||
|
m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;"));
|
||||||
|
root->addWidget(m_actionArea);
|
||||||
|
|
||||||
|
// Thin separator
|
||||||
|
auto *sep = new QFrame(this);
|
||||||
|
sep->setFrameShape(QFrame::HLine);
|
||||||
|
sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;"));
|
||||||
|
root->addWidget(sep);
|
||||||
|
|
||||||
|
// Games header row
|
||||||
|
auto *gh = new QHBoxLayout;
|
||||||
|
gh->setContentsMargins(10, 4, 8, 2);
|
||||||
|
auto *gl = new QLabel(tr("Games"), this);
|
||||||
|
gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;"));
|
||||||
|
gh->addWidget(gl);
|
||||||
|
gh->addStretch();
|
||||||
|
m_refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||||
|
m_refreshBtn->setFixedSize(20, 20);
|
||||||
|
m_refreshBtn->setFlat(true);
|
||||||
|
m_refreshBtn->setStyleSheet(
|
||||||
|
QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}"
|
||||||
|
"QPushButton:hover{color:white;}"));
|
||||||
|
connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||||
|
gh->addWidget(m_refreshBtn);
|
||||||
|
root->addLayout(gh);
|
||||||
|
|
||||||
|
// Status label
|
||||||
|
m_gamesStatus = new QLabel(this);
|
||||||
|
m_gamesStatus->setAlignment(Qt::AlignCenter);
|
||||||
|
m_gamesStatus->setStyleSheet(
|
||||||
|
QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;"));
|
||||||
|
root->addWidget(m_gamesStatus);
|
||||||
|
|
||||||
|
// Games list
|
||||||
|
m_gamesModel = new QStandardItemModel(this);
|
||||||
|
m_gamesView = new QListView(this);
|
||||||
|
m_gamesView->setModel(m_gamesModel);
|
||||||
|
m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView));
|
||||||
|
m_gamesView->setFrameShape(QFrame::NoFrame);
|
||||||
|
m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
|
m_gamesView->setMaximumHeight(220);
|
||||||
|
m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}"
|
||||||
|
"QListView::item:selected{background:#232e42;}"));
|
||||||
|
root->addWidget(m_gamesView);
|
||||||
|
|
||||||
|
// Close button — positioned absolutely in the top-right corner
|
||||||
|
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
||||||
|
m_closeBtn->setFixedSize(22, 22);
|
||||||
|
m_closeBtn->setFlat(true);
|
||||||
|
m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;"
|
||||||
|
"border:none;border-radius:11px;font-size:10px;}"
|
||||||
|
"QPushButton:hover{color:white;background:rgba(200,50,50,200);}"));
|
||||||
|
connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Action button factory ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p)
|
||||||
|
{
|
||||||
|
auto *b = new QPushButton(label, p);
|
||||||
|
b->setToolTip(tip);
|
||||||
|
b->setFixedHeight(26);
|
||||||
|
b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
b->setStyleSheet(QStringLiteral("QPushButton{"
|
||||||
|
" background:#192030;color:#b8c8de;border:1px solid #263040;"
|
||||||
|
" border-radius:4px;font-size:11px;padding:0 4px;"
|
||||||
|
"}"
|
||||||
|
"QPushButton:hover{background:#223050;color:white;}"
|
||||||
|
"QPushButton:pressed{background:#162030;}"
|
||||||
|
"QPushButton:disabled{color:#384858;border-color:#192030;}"));
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||||
|
{
|
||||||
|
// Clear previous contents
|
||||||
|
delete m_actionArea->layout();
|
||||||
|
const auto old = m_actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
|
||||||
|
for (auto *w : old) {
|
||||||
|
w->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString name = QString::fromStdString(userInfo.name());
|
||||||
|
const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level());
|
||||||
|
const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name()));
|
||||||
|
const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator);
|
||||||
|
const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin);
|
||||||
|
const auto their = UserLevelFlags(userInfo.user_level());
|
||||||
|
const bool isReg = their.testFlag(ServerInfo_User::IsRegistered);
|
||||||
|
|
||||||
|
auto *grid = new QGridLayout(m_actionArea);
|
||||||
|
grid->setContentsMargins(8, 6, 8, 6);
|
||||||
|
grid->setSpacing(4);
|
||||||
|
|
||||||
|
int row = 0, col = 0;
|
||||||
|
const int cols = 3;
|
||||||
|
auto add = [&](QPushButton *btn) {
|
||||||
|
grid->addWidget(btn, row, col);
|
||||||
|
if (++col == cols) {
|
||||||
|
col = 0;
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Always visible ────────────────────────────────────────────────────────
|
||||||
|
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea);
|
||||||
|
chat->setEnabled(!isSelf && online);
|
||||||
|
connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); });
|
||||||
|
add(chat);
|
||||||
|
|
||||||
|
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea);
|
||||||
|
connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); });
|
||||||
|
add(prof);
|
||||||
|
|
||||||
|
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea);
|
||||||
|
games->setEnabled(!isSelf && online);
|
||||||
|
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||||
|
add(games);
|
||||||
|
|
||||||
|
// ── Buddy / ignore (registered users only) ────────────────────────────────
|
||||||
|
if (!isSelf && isReg) {
|
||||||
|
if (isBuddy) {
|
||||||
|
auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); });
|
||||||
|
add(b);
|
||||||
|
} else {
|
||||||
|
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); });
|
||||||
|
add(b);
|
||||||
|
}
|
||||||
|
if (isIgnored) {
|
||||||
|
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); });
|
||||||
|
add(b);
|
||||||
|
} else {
|
||||||
|
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); });
|
||||||
|
add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Moderator actions ─────────────────────────────────────────────────────
|
||||||
|
if (!isSelf && (isMod || isAdmin)) {
|
||||||
|
if (col != 0) {
|
||||||
|
++row;
|
||||||
|
col = 0;
|
||||||
|
} // start mod section on a fresh row
|
||||||
|
|
||||||
|
auto *ban = makeBtn(tr("Ban"), tr("Ban from server"), m_actionArea);
|
||||||
|
auto *warn = makeBtn(tr("Warn"), tr("Warn user"), m_actionArea);
|
||||||
|
auto *bLog = makeBtn(tr("Ban log"), tr("View ban history"), m_actionArea);
|
||||||
|
auto *wLog = makeBtn(tr("Warn log"), tr("View warning history"), m_actionArea);
|
||||||
|
connect(ban, &QPushButton::clicked, this, [this, name] { emit banRequested(name); });
|
||||||
|
connect(warn, &QPushButton::clicked, this, [this, name] { emit warnRequested(name); });
|
||||||
|
connect(bLog, &QPushButton::clicked, this, [this, name] { emit banHistoryRequested(name); });
|
||||||
|
connect(wLog, &QPushButton::clicked, this, [this, name] { emit warnHistoryRequested(name); });
|
||||||
|
add(ban);
|
||||||
|
add(warn);
|
||||||
|
add(bLog);
|
||||||
|
add(wLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Admin actions ─────────────────────────────────────────────────────────
|
||||||
|
if (!isSelf && isAdmin) {
|
||||||
|
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea);
|
||||||
|
connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); });
|
||||||
|
add(notes);
|
||||||
|
|
||||||
|
if (their.testFlag(ServerInfo_User::IsModerator)) {
|
||||||
|
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); });
|
||||||
|
add(b);
|
||||||
|
} else if (isReg) {
|
||||||
|
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); });
|
||||||
|
add(b);
|
||||||
|
}
|
||||||
|
if (their.testFlag(ServerInfo_User::IsJudge)) {
|
||||||
|
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); });
|
||||||
|
add(b);
|
||||||
|
} else if (isReg) {
|
||||||
|
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea);
|
||||||
|
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); });
|
||||||
|
add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_actionArea->adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── showForUser ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UserInfoPopup::showForUser(const QString &userName,
|
||||||
|
const ServerInfo_User &userInfo,
|
||||||
|
bool online,
|
||||||
|
bool isBuddy,
|
||||||
|
bool isIgnored)
|
||||||
|
{
|
||||||
|
m_currentUser = userName;
|
||||||
|
m_currentUserInfo = userInfo;
|
||||||
|
m_currentOnline = online;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const QPixmap avatar = m_avatarCache ? m_avatarCache->value(userName) : QPixmap{};
|
||||||
|
const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName))
|
||||||
|
? m_cardArtParamsMap->value(userName)
|
||||||
|
: CardArtParams{};
|
||||||
|
const QString artKey = userName + u'|' + params.cardName;
|
||||||
|
const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{};
|
||||||
|
m_header->setUserData(userInfo, online, avatar, cardArt, params);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
||||||
|
|
||||||
|
// Games list reset
|
||||||
|
m_gamesModel->clear();
|
||||||
|
m_gamesView->hide();
|
||||||
|
m_gamesStatus->setText(tr("Loading games…"));
|
||||||
|
m_gamesStatus->show();
|
||||||
|
|
||||||
|
// Close button — top-right corner, above everything
|
||||||
|
m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6);
|
||||||
|
m_closeBtn->raise();
|
||||||
|
|
||||||
|
adjustSize();
|
||||||
|
fetchGames();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Games fetch ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void UserInfoPopup::fetchGames()
|
||||||
|
{
|
||||||
|
if (!m_client || m_currentUser.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command_GetGamesOfUser cmd;
|
||||||
|
cmd.set_user_name(m_currentUser.toStdString());
|
||||||
|
|
||||||
|
const QString snapshot = m_currentUser;
|
||||||
|
PendingCommand *pend = m_client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this,
|
||||||
|
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
|
||||||
|
m_client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
|
||||||
|
{
|
||||||
|
if (forUser != m_currentUser) {
|
||||||
|
return; // stale response — different user showing now
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gamesModel->clear();
|
||||||
|
|
||||||
|
if (r.response_code() != Response::RespOk) {
|
||||||
|
m_gamesStatus->setText(tr("Could not load games."));
|
||||||
|
m_gamesStatus->show();
|
||||||
|
m_gamesView->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
|
||||||
|
if (resp.game_list_size() == 0) {
|
||||||
|
m_gamesStatus->setText(tr("No active games."));
|
||||||
|
m_gamesStatus->show();
|
||||||
|
m_gamesView->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < resp.game_list_size(); ++i) {
|
||||||
|
auto *item = new QStandardItem;
|
||||||
|
item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData);
|
||||||
|
item->setEditable(false);
|
||||||
|
m_gamesModel->appendRow(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gamesStatus->hide();
|
||||||
|
m_gamesView->show();
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserInfoPopup::refreshGames()
|
||||||
|
{
|
||||||
|
m_gamesModel->clear();
|
||||||
|
m_gamesView->hide();
|
||||||
|
m_gamesStatus->setText(tr("Loading games…"));
|
||||||
|
m_gamesStatus->show();
|
||||||
|
fetchGames();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mouse events ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
void UserInfoPopup::enterEvent(QEnterEvent *e)
|
||||||
|
{
|
||||||
|
QFrame::enterEvent(e);
|
||||||
|
emit mouseEnteredPopup();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void UserInfoPopup::enterEvent(QEvent *e)
|
||||||
|
{
|
||||||
|
QFrame::enterEvent(e);
|
||||||
|
emit mouseEnteredPopup();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
void UserInfoPopup::leaveEvent(QEvent *e)
|
||||||
|
{
|
||||||
|
QFrame::leaveEvent(e);
|
||||||
|
emit mouseLeftPopup();
|
||||||
|
}
|
||||||
174
cockatrice/src/interface/widgets/server/user/user_info_popup.h
Normal file
174
cockatrice/src/interface/widgets/server/user/user_info_popup.h
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
#ifndef COCKATRICE_USER_INFO_POPUP_H
|
||||||
|
#define COCKATRICE_USER_INFO_POPUP_H
|
||||||
|
|
||||||
|
#include "../../interface/widgets/server/game_type_map.h"
|
||||||
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QListView>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <libcockatrice/network/server/remote/user_level.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||||
|
|
||||||
|
class AbstractClient;
|
||||||
|
class QLabel;
|
||||||
|
class QPushButton;
|
||||||
|
class TabSupervisor;
|
||||||
|
|
||||||
|
// ── Roles ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace PopupRoles
|
||||||
|
{
|
||||||
|
constexpr int GameData = Qt::UserRole + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header widget ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class UserInfoHeaderWidget
|
||||||
|
* @brief Paints the enlarged banner card art + circular avatar section at the
|
||||||
|
* top of the UserInfoPopup.
|
||||||
|
*
|
||||||
|
* Layout mirrors UserListPainter but at a larger scale: the card art fills the
|
||||||
|
* full width as a semi-transparent background, a bottom gradient ensures the
|
||||||
|
* avatar and username text remain legible, and the status ring colour matches
|
||||||
|
* the UserListPainter convention.
|
||||||
|
*/
|
||||||
|
class UserInfoHeaderWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
static constexpr int HeaderHeight = 130;
|
||||||
|
static constexpr int AvatarSize = 68;
|
||||||
|
static constexpr int AvatarPawnSize = 46;
|
||||||
|
static constexpr int LeftPad = 14;
|
||||||
|
static constexpr int AvatarToTextGap = 10;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UserInfoHeaderWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setUserData(const ServerInfo_User &user,
|
||||||
|
bool online,
|
||||||
|
const QPixmap &avatar,
|
||||||
|
const QPixmap &cardArt,
|
||||||
|
const CardArtParams ¶ms);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ServerInfo_User m_user;
|
||||||
|
bool m_online = false;
|
||||||
|
QPixmap m_avatar;
|
||||||
|
QPixmap m_cardArt;
|
||||||
|
CardArtParams m_params;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Main popup ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class UserInfoPopup
|
||||||
|
* @brief Floating panel showing an enlarged user card, quick action buttons,
|
||||||
|
* and a live scrollable games list.
|
||||||
|
*
|
||||||
|
* Lifecycle (mirrors DeckEditorDeckDockWidget):
|
||||||
|
* - showForUser() — populate, position externally, call show()
|
||||||
|
* - mouseEnteredPopup / mouseLeftPopup — caller manages hide timer
|
||||||
|
* - closeRequested() — emitted by the internal close button
|
||||||
|
*
|
||||||
|
* The popup is a Qt::Tool frameless child so windowOpacity animations and
|
||||||
|
* move() in screen coordinates work identically to CardInfoPictureEnlargedWidget.
|
||||||
|
*
|
||||||
|
* Action signals map 1-to-1 to UserContextMenu::exec*() methods so all action
|
||||||
|
* logic stays in one place.
|
||||||
|
*/
|
||||||
|
class UserInfoPopup : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
static constexpr int PopupWidth = 316;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit UserInfoPopup(TabSupervisor *tabSupervisor,
|
||||||
|
AbstractClient *client,
|
||||||
|
const QMap<QString, QPixmap> *avatarCache,
|
||||||
|
const QMap<QString, QPixmap> *cardArtCache,
|
||||||
|
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||||
|
QWidget *parent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the popup for @p userName and kick off a game list fetch.
|
||||||
|
* Call show() / move() externally after this.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||||
|
void fetchGames();
|
||||||
|
|
||||||
|
[[nodiscard]] QString currentUser() const
|
||||||
|
{
|
||||||
|
return m_currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void mouseEnteredPopup();
|
||||||
|
void mouseLeftPopup();
|
||||||
|
void closeRequested();
|
||||||
|
|
||||||
|
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
|
||||||
|
void chatRequested(const QString &userName);
|
||||||
|
void detailsRequested(const QString &userName);
|
||||||
|
void showGamesRequested(const QString &userName);
|
||||||
|
void addBuddyRequested(const QString &userName);
|
||||||
|
void removeBuddyRequested(const QString &userName);
|
||||||
|
void addIgnoreRequested(const QString &userName);
|
||||||
|
void removeIgnoreRequested(const QString &userName);
|
||||||
|
void banRequested(const QString &userName);
|
||||||
|
void warnRequested(const QString &userName);
|
||||||
|
void banHistoryRequested(const QString &userName);
|
||||||
|
void warnHistoryRequested(const QString &userName);
|
||||||
|
void adminNotesRequested(const QString &userName);
|
||||||
|
void promoteToModRequested(const QString &userName);
|
||||||
|
void demoteFromModRequested(const QString &userName);
|
||||||
|
void promoteToJudgeRequested(const QString &userName);
|
||||||
|
void demoteFromJudgeRequested(const QString &userName);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
void enterEvent(QEnterEvent *e) override;
|
||||||
|
#else
|
||||||
|
void enterEvent(QEvent *e) override;
|
||||||
|
#endif
|
||||||
|
void leaveEvent(QEvent *e) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void refreshGames();
|
||||||
|
void onGamesReceived(const Response &r, const QString &forUser);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildUi();
|
||||||
|
void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||||
|
|
||||||
|
TabSupervisor *m_ts;
|
||||||
|
AbstractClient *m_client;
|
||||||
|
const QMap<QString, QPixmap> *m_avatarCache;
|
||||||
|
const QMap<QString, QPixmap> *m_cardArtCache;
|
||||||
|
const QMap<QString, CardArtParams> *m_cardArtParamsMap;
|
||||||
|
|
||||||
|
QString m_currentUser;
|
||||||
|
ServerInfo_User m_currentUserInfo;
|
||||||
|
bool m_currentOnline = false;
|
||||||
|
|
||||||
|
UserInfoHeaderWidget *m_header;
|
||||||
|
QWidget *m_actionArea; ///< rebuilt per user
|
||||||
|
QListView *m_gamesView;
|
||||||
|
QStandardItemModel *m_gamesModel;
|
||||||
|
QLabel *m_gamesStatus;
|
||||||
|
QPushButton *m_closeBtn;
|
||||||
|
QPushButton *m_refreshBtn;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_USER_INFO_POPUP_H
|
||||||
|
|
@ -489,6 +489,59 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
userTree->header()->setStretchLastSection(true);
|
userTree->header()->setStretchLastSection(true);
|
||||||
|
|
||||||
|
// ── Hover popup ───────────────────────────────────────────────────────────
|
||||||
|
m_userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
|
||||||
|
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||||
|
window()); // parented to main window so it floats above siblings
|
||||||
|
|
||||||
|
m_userInfoPopup->hide();
|
||||||
|
m_userInfoPopup->setWindowOpacity(0.0);
|
||||||
|
m_userInfoPopup->installEventFilter(this);
|
||||||
|
|
||||||
|
connectPopupSignals();
|
||||||
|
|
||||||
|
m_showPopupTimer = new QTimer(this);
|
||||||
|
m_showPopupTimer->setSingleShot(true);
|
||||||
|
m_showPopupTimer->setInterval(280);
|
||||||
|
connect(m_showPopupTimer, &QTimer::timeout, this, [this] {
|
||||||
|
if (!m_hoveredUser.isEmpty()) {
|
||||||
|
showPopupForUser(m_hoveredUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_hidePopupTimer = new QTimer(this);
|
||||||
|
m_hidePopupTimer->setSingleShot(true);
|
||||||
|
m_hidePopupTimer->setInterval(160);
|
||||||
|
connect(m_hidePopupTimer, &QTimer::timeout, this, [this] {
|
||||||
|
if (!m_popupPinned && !m_userInfoPopup->underMouse() && !userTree->underMouse()) {
|
||||||
|
hidePopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
userTree->setMouseTracking(true);
|
||||||
|
userTree->viewport()->setMouseTracking(true);
|
||||||
|
userTree->viewport()->installEventFilter(this);
|
||||||
|
|
||||||
|
// Pin on item click
|
||||||
|
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||||
|
if (!SettingsCache::instance().getStyleUserList()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString name = static_cast<UserListTWI *>(item)->getUserInfo().name().c_str();
|
||||||
|
m_popupPinned = false; // reset so showPopupForUser can update
|
||||||
|
showPopupForUser(name);
|
||||||
|
m_popupPinned = true; // pin after showing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unpin when selection cleared
|
||||||
|
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||||
|
[this](const QItemSelection &sel, const QItemSelection &) {
|
||||||
|
if (sel.isEmpty() && m_popupPinned) {
|
||||||
|
m_popupPinned = false;
|
||||||
|
hidePopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this,
|
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this,
|
||||||
[this](const QString &) { userTree->viewport()->update(); });
|
[this](const QString &) { userTree->viewport()->update(); });
|
||||||
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
|
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this,
|
||||||
|
|
@ -533,6 +586,160 @@ void UserListWidget::applyDisplayMode()
|
||||||
userTree->viewport()->update();
|
userTree->viewport()->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserListWidget::connectPopupSignals()
|
||||||
|
{
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::closeRequested, this, [this] {
|
||||||
|
m_popupPinned = false;
|
||||||
|
hidePopup(true);
|
||||||
|
});
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::mouseEnteredPopup, m_hidePopupTimer, &QTimer::stop);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::mouseLeftPopup, this, [this] {
|
||||||
|
if (!m_popupPinned) {
|
||||||
|
m_hidePopupTimer->start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wire all action signals to UserContextMenu::exec*()
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::removeBuddyRequested, userContextMenu,
|
||||||
|
&UserContextMenu::execRemoveFromBuddy);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::addIgnoreRequested, userContextMenu, &UserContextMenu::execAddToIgnore);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::removeIgnoreRequested, userContextMenu,
|
||||||
|
&UserContextMenu::execRemoveFromIgnore);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::banRequested, userContextMenu, &UserContextMenu::execBan);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::warnRequested, userContextMenu, &UserContextMenu::execWarn);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::banHistoryRequested, userContextMenu, &UserContextMenu::execBanHistory);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::warnHistoryRequested, userContextMenu, &UserContextMenu::execWarnHistory);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::adminNotesRequested, userContextMenu, &UserContextMenu::execAdminNotes);
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::promoteToModRequested, this,
|
||||||
|
[this](const QString &n) { userContextMenu->execAdjustMod(n, true, false); });
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::demoteFromModRequested, this,
|
||||||
|
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); });
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::promoteToJudgeRequested, this,
|
||||||
|
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, true); });
|
||||||
|
connect(m_userInfoPopup, &UserInfoPopup::demoteFromJudgeRequested, this,
|
||||||
|
[this](const QString &n) { userContextMenu->execAdjustMod(n, false, false); });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
||||||
|
{
|
||||||
|
if (obj == userTree->viewport()) {
|
||||||
|
if (event->type() == QEvent::MouseMove) {
|
||||||
|
if (!SettingsCache::instance().getStyleUserList()) {
|
||||||
|
return QGroupBox::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
auto *me = static_cast<QMouseEvent *>(event);
|
||||||
|
auto *twi = static_cast<UserListTWI *>(userTree->itemAt(me->pos()));
|
||||||
|
const QString hovName = twi ? QString::fromStdString(twi->getUserInfo().name()) : QString{};
|
||||||
|
|
||||||
|
if (hovName != m_hoveredUser) {
|
||||||
|
m_hoveredUser = hovName;
|
||||||
|
if (!hovName.isEmpty()) {
|
||||||
|
m_hidePopupTimer->stop();
|
||||||
|
if (!m_popupPinned) {
|
||||||
|
m_showPopupTimer->start();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_showPopupTimer->stop();
|
||||||
|
if (!m_popupPinned) {
|
||||||
|
m_hidePopupTimer->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event->type() == QEvent::Leave) {
|
||||||
|
m_hoveredUser.clear();
|
||||||
|
m_showPopupTimer->stop();
|
||||||
|
if (!m_popupPinned) {
|
||||||
|
m_hidePopupTimer->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QGroupBox::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserListWidget::showPopupForUser(const QString &userName)
|
||||||
|
{
|
||||||
|
UserListTWI *item = users.value(userName);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerInfo_User &info = item->getUserInfo();
|
||||||
|
const bool online = item->data(0, UserListRoles::Online).toBool();
|
||||||
|
const bool isBuddy = userContextMenu->getUserListProxy()->isUserBuddy(userName);
|
||||||
|
const bool isIgn = userContextMenu->getUserListProxy()->isUserIgnored(userName);
|
||||||
|
|
||||||
|
m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn);
|
||||||
|
|
||||||
|
positionPopup(userName);
|
||||||
|
|
||||||
|
m_userInfoPopup->show();
|
||||||
|
m_userInfoPopup->raise();
|
||||||
|
|
||||||
|
// Fade in
|
||||||
|
m_userInfoPopup->setWindowOpacity(0.0);
|
||||||
|
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup);
|
||||||
|
fade->setDuration(120);
|
||||||
|
fade->setStartValue(0.0);
|
||||||
|
fade->setEndValue(1.0);
|
||||||
|
fade->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserListWidget::positionPopup(const QString &userName)
|
||||||
|
{
|
||||||
|
UserListTWI *item = users.value(userName);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget *vp = userTree->viewport();
|
||||||
|
const QRect itemR = userTree->visualItemRect(item);
|
||||||
|
const QPoint itemTL = vp->mapToGlobal(itemR.topLeft());
|
||||||
|
const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft());
|
||||||
|
|
||||||
|
const int popW = m_userInfoPopup->width();
|
||||||
|
const int popH = m_userInfoPopup->sizeHint().height();
|
||||||
|
const int margin = 12;
|
||||||
|
|
||||||
|
// Go left of the list if there's room, otherwise right
|
||||||
|
int x =
|
||||||
|
(vpTL.x() >= popW + margin) ? vpTL.x() - popW - margin : vp->mapToGlobal(vp->rect().topRight()).x() + margin;
|
||||||
|
|
||||||
|
// Align top with the hovered row, clamped to available screen space
|
||||||
|
const QRect screen = QGuiApplication::primaryScreen()->availableGeometry();
|
||||||
|
int y = itemTL.y();
|
||||||
|
y = qMin(y, screen.bottom() - popH - margin);
|
||||||
|
y = qMax(y, screen.top() + margin);
|
||||||
|
|
||||||
|
m_userInfoPopup->move(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserListWidget::hidePopup(bool immediate)
|
||||||
|
{
|
||||||
|
m_showPopupTimer->stop();
|
||||||
|
m_hidePopupTimer->stop();
|
||||||
|
if (!m_userInfoPopup->isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
m_userInfoPopup->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade out
|
||||||
|
auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup);
|
||||||
|
fade->setDuration(100);
|
||||||
|
fade->setStartValue(m_userInfoPopup->windowOpacity());
|
||||||
|
fade->setEndValue(0.0);
|
||||||
|
connect(fade, &QPropertyAnimation::finished, m_userInfoPopup, &QWidget::hide);
|
||||||
|
fade->start(QAbstractAnimation::DeleteWhenStopped);
|
||||||
|
}
|
||||||
|
|
||||||
void UserListWidget::retranslateUi()
|
void UserListWidget::retranslateUi()
|
||||||
{
|
{
|
||||||
userContextMenu->retranslateUi();
|
userContextMenu->retranslateUi();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include "../../cards/card_info_picture_art_crop_widget.h"
|
#include "../../cards/card_info_picture_art_crop_widget.h"
|
||||||
#include "user_avatar_provider.h"
|
#include "user_avatar_provider.h"
|
||||||
#include "user_card_art_provider.h"
|
#include "user_card_art_provider.h"
|
||||||
|
#include "user_info_popup.h"
|
||||||
#include "user_list_manager.h"
|
#include "user_list_manager.h"
|
||||||
#include "user_list_painter.h"
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
|
|
@ -151,6 +152,17 @@ private:
|
||||||
UserAvatarProvider *avatarProvider = nullptr;
|
UserAvatarProvider *avatarProvider = nullptr;
|
||||||
UserCardArtProvider *cardArtProvider = nullptr;
|
UserCardArtProvider *cardArtProvider = nullptr;
|
||||||
QMap<QString, CardArtParams> cardArtParamsMap;
|
QMap<QString, CardArtParams> cardArtParamsMap;
|
||||||
|
// ── Hover popup ───────────────────────────────────────────────────────────
|
||||||
|
UserInfoPopup *m_userInfoPopup = nullptr;
|
||||||
|
QTimer *m_showPopupTimer = nullptr;
|
||||||
|
QTimer *m_hidePopupTimer = nullptr;
|
||||||
|
QString m_hoveredUser;
|
||||||
|
bool m_popupPinned = false;
|
||||||
|
|
||||||
|
void showPopupForUser(const QString &userName);
|
||||||
|
void hidePopup(bool immediate = false);
|
||||||
|
void positionPopup(const QString &userName);
|
||||||
|
void connectPopupSignals();
|
||||||
|
|
||||||
QMap<QString, UserListTWI *> users;
|
QMap<QString, UserListTWI *> users;
|
||||||
TabSupervisor *tabSupervisor;
|
TabSupervisor *tabSupervisor;
|
||||||
|
|
@ -178,6 +190,7 @@ public:
|
||||||
QWidget *parent = nullptr);
|
QWidget *parent = nullptr);
|
||||||
void bind(UserListManager *mgr);
|
void bind(UserListManager *mgr);
|
||||||
void applyDisplayMode();
|
void applyDisplayMode();
|
||||||
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
void rebuild();
|
void rebuild();
|
||||||
void processUserInfo(const ServerInfo_User &user, bool online);
|
void processUserInfo(const ServerInfo_User &user, bool online);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue