Compare commits

..

No commits in common. "d99798111eb92cb380df5b81ed4d643cb6eb8aea" and "6ee1fd58e69bf0fe5689fef273b863190fdd0ae1" have entirely different histories.

36 changed files with 431 additions and 2305 deletions

View file

@ -260,7 +260,6 @@ set(cockatrice_SOURCES
src/interface/widgets/server/user/user_info_connection.cpp
src/interface/widgets/server/user/user_list_manager.cpp
src/interface/widgets/server/user/user_list_painter.cpp
src/interface/widgets/server/user/user_list_panel_widget.cpp
src/interface/widgets/server/user/user_list_widget.cpp
src/interface/widgets/settings_page/abstract_settings_page.cpp
src/interface/widgets/settings_page/appearance_settings_page.cpp
@ -397,8 +396,6 @@ set(cockatrice_SOURCES
src/interface/intents/intent_join_server_room.h
src/interface/intents/intent_login.cpp
src/interface/intents/intent_login.h
src/interface/intents/intent_open_server_room_by_name.cpp
src/interface/intents/intent_open_server_room_by_name.h
src/interface/intents/url_parser.cpp
src/interface/intents/url_parser.h
src/interface/widgets/server/user/user_info_popup.cpp

View file

@ -1,183 +0,0 @@
#include "intent_open_server_room_by_name.h"
#include "../widgets/tabs/tab_room.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_connect_to_server.h"
#include <libcockatrice/protocol/pb/event_list_rooms.pb.h>
#include <libcockatrice/protocol/pb/response_join_room.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
IntentOpenServerRoomByName::IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinRoom> _context,
const QString &_roomName)
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()),
roomName(_roomName)
{
checkTimer.setInterval(250);
connect(&checkTimer, &QTimer::timeout, this, [this]() {
if (selectOpenRoom()) {
checkTimer.stop();
}
});
}
bool IntentOpenServerRoomByName::checkPrecondition() const
{
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
// peerPort() reflects the actual TCP peer, which may differ from the
// configured server port (e.g. when connecting through a proxy), so only
// the hostname is compared here.
if (remoteClient->peerName() != context->serverContext.hostname) {
return false;
}
if (QString::number(remoteClient->peerPort()) != context->serverContext.port) {
return false;
}
return true;
}
void IntentOpenServerRoomByName::onPreconditionSatisfied()
{
if (listening) {
return;
}
listening = true;
if (selectOpenRoom()) {
return;
}
// The room selector is the component that requests the room list, so the
// server tab must exist for the room to be resolved by name.
if (!tabSupervisor->getTabServer()) {
tabSupervisor->openTabServer();
}
if (!tabSupervisor->getTabServer()) {
emitFailed(tr("No server tab available"));
return;
}
connect(remoteClient, &RemoteClient::listRoomsEventReceived, this, &IntentOpenServerRoomByName::processListRooms);
connect(remoteClient, &RemoteClient::statusChanged, this, &IntentOpenServerRoomByName::onClientStatusChanged);
// The room tab may be opened by our own join, by the room selector's auto-join, or by a
// join that was already in flight. Poll until it shows up.
checkTimer.start();
// While no join has been sent yet, keep the room list fresh: the list may have been
// requested before we subscribed to it, or a response may have been dropped during a
// busy login burst. A stale list would otherwise leave the room unresolved forever.
connect(&refreshTimer, &QTimer::timeout, this, [this]() {
if (!joinPending) {
remoteClient->sendCommand(remoteClient->prepareSessionCommand(Command_ListRooms()));
}
});
refreshTimer.setInterval(5000);
refreshTimer.start();
// Last-resort failure for "the room genuinely is not in a fresh list". This must NOT
// fire while a join is in flight: a loaded server may take longer than that to answer
// during a login burst, and killing the intent early would leave the connection
// registered in the room with no tab to display it and every later join attempt
// would then be rejected with RespContextError.
QTimer::singleShot(20000, this, [this]() {
if (!joinPending) {
emitFailed(tr("Timed out while looking for the server room %1").arg(roomName));
}
});
}
void IntentOpenServerRoomByName::onPreconditionNotSatisfied()
{
runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
}
void IntentOpenServerRoomByName::onClientStatusChanged(ClientStatus status)
{
if (status != ClientStatus::StatusLoggedIn) {
emitFailed(tr("Disconnected while looking for the server room %1").arg(roomName));
}
}
bool IntentOpenServerRoomByName::selectOpenRoom()
{
const auto &roomTabs = tabSupervisor->getRoomTabs();
for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) {
TabRoom *room = i.value();
if (room->getRoomName() == roomName) {
tabSupervisor->setCurrentWidget(room);
emitFinished();
return true;
}
}
return false;
}
void IntentOpenServerRoomByName::processListRooms(const Event_ListRooms &event)
{
if (selectOpenRoom()) {
return;
}
for (int i = 0; i < event.room_list_size(); ++i) {
const ServerInfo_Room &room = event.room_list(i);
if (room.has_name() && QString::fromStdString(room.name()) == roomName) {
openRoom(room);
return;
}
}
}
void IntentOpenServerRoomByName::openRoom(const ServerInfo_Room &roomInfo)
{
if (joinPending) {
return;
}
joinPending = true;
// Rooms flagged auto_join are joined by the room selector automatically. Sending our own
// Command_JoinRoom on top of that would be answered with RespContextError.
if (roomInfo.has_auto_join() && roomInfo.auto_join()) {
return;
}
Command_JoinRoom cmd;
cmd.set_room_id(roomInfo.room_id());
PendingCommand *pend = remoteClient->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this,
[this](const Response &r, const CommandContainer &, const QVariant &) { handleJoinResponse(r); });
remoteClient->sendCommand(pend);
}
void IntentOpenServerRoomByName::handleJoinResponse(const Response &response)
{
switch (response.response_code()) {
case Response::RespOk: {
const Response_JoinRoom &resp = response.GetExtension(Response_JoinRoom::ext);
if (!tabSupervisor->getRoomTabs().contains(resp.room_info().room_id())) {
tabSupervisor->addRoomTab(resp.room_info(), true);
}
emitFinished();
return;
}
case Response::RespNameNotFound:
emitFailed(tr("Failed to join the server room %1: it doesn't exist on the server.").arg(roomName));
return;
case Response::RespUserLevelTooLow:
emitFailed(tr("You do not have the required permission to join the server room %1.").arg(roomName));
return;
case Response::RespContextError:
// The room was already joined by someone else (e.g. the room selector's
// auto-join). It will show up in the room tabs shortly, so keep waiting.
return;
default:
emitFailed(tr("Failed to join the server room %1 due to an unknown error.").arg(roomName));
return;
}
}

View file

@ -1,61 +0,0 @@
#ifndef COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H
#define COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H
#include "contexts/context_join_room.h"
#include "intent.h"
#include "remote_client.h"
#include <QScopedPointer>
#include <QString>
#include <QTimer>
#include <memory>
class TabRoom;
class TabSupervisor;
class Event_ListRooms;
class ServerInfo_Room;
/**
* @brief Connects to the configured server and opens a room identified by its name.
*
* Room ids are assigned by the server per session, so the room is resolved by name from the
* room list once the client is logged in. If the room is already open it is simply selected.
*
* The join itself is sent directly through the client instead of `TabServer::joinRoom`, so a
* failed join only fails the intent silently instead of popping a modal error box during
* startup. Success is routed to `TabSupervisor::addRoomTab`, the same tab-creation machinery
* the normal join flow uses.
*/
class IntentOpenServerRoomByName : public Intent
{
Q_OBJECT
public:
IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinRoom> _context,
const QString &_roomName);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
void processListRooms(const Event_ListRooms &event);
void openRoom(const ServerInfo_Room &roomInfo);
void handleJoinResponse(const Response &response);
void onClientStatusChanged(ClientStatus status);
bool selectOpenRoom();
TabSupervisor *tabSupervisor;
RemoteClient *remoteClient;
QScopedPointer<ContextJoinRoom> context;
QString roomName;
bool listening = false;
bool joinPending = false;
QTimer checkTimer;
QTimer refreshTimer;
};
#endif // COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
#include "../../interface/pixel_map_generator.h"
#include <QAbstractScrollArea>
#include <QApplication>
#include <QPainter>
#include <QPainterPath>
#include <QScrollBar>
@ -20,29 +19,6 @@ QSize UserListPainter::sizeHint()
return QSize(0, RowHeight);
}
UserListPainter::Style UserListPainter::resolveStyle(const QPalette &palette, bool dark)
{
Style style;
style.dark = dark;
const QColor base = palette.color(QPalette::Base);
const QColor alt = palette.color(QPalette::AlternateBase);
style.cardStart = base;
style.cardEnd = (alt != base) ? alt : palette.color(QPalette::Midlight);
style.base = base;
style.textOnline = palette.color(QPalette::Text);
style.textOffline = palette.color(QPalette::Disabled, QPalette::Text);
style.ringOffline = palette.color(QPalette::Disabled, QPalette::Text);
style.dropShadow = dark;
return style;
}
QColor UserListPainter::blend(const QColor &a, const QColor &b, qreal t)
{
const qreal u = 1.0 - t;
return QColor(qRound(a.red() * u + b.red() * t), qRound(a.green() * u + b.green() * t),
qRound(a.blue() * u + b.blue() * t), qRound(a.alpha() * u + b.alpha() * t));
}
QColor UserListPainter::getAccentColor(const UserLevelFlags &userLevel, bool online)
{
QColor accentColor;
@ -83,41 +59,18 @@ int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRec
void UserListPainter::drawBackground(QPainter *painter,
const QRectF &cardRect,
const QColor &accentColor,
bool selected,
const Style &style,
bool hasRole)
bool selected)
{
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
if (style.dark) {
// Dark mode darkens the role color to fit the dark surface and fades
// it into the deep navy surface on the right. The text drop shadow
// keeps the username legible over the colored edge.
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
} else if (hasRole) {
// Light mode pegs the role color on the left at near full strength
// and fades it into the white surface on the right. The tint stays
// bright enough that the dark text remains legible without a shadow.
bg.setColorAt(0, blend(style.cardStart, accentColor, selected ? 0.75 : 0.65));
bg.setColorAt(1, blend(style.cardEnd, accentColor, selected ? 0.18 : 0.10));
} else {
// Regular users are the light theme's neutral paper cards. A flat
// warm card fill (the normal row surface, slightly deepened) keeps
// every row clearly visible without borrowing a role color. Selection
// shifts the fill toward a soft slate so the highlight still reads.
const QColor paper = style.cardEnd.darker(108);
bg.setColorAt(0, blend(paper, accentColor, selected ? 0.35 : 0.0));
bg.setColorAt(1, blend(paper, accentColor, selected ? 0.25 : 0.0));
}
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);
if (style.dark || hasRole || selected) {
painter->setBrush(accentColor);
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
}
painter->setBrush(accentColor);
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
}
static QString makeKey(const QString &user, const QString &card, const QString &providerId)
@ -210,8 +163,7 @@ void UserListPainter::drawAvatar(QPainter *painter,
const UserLevelFlags &userLevel,
const ServerInfo_User &userInfo,
const QString &privLevel,
const QMap<QString, QPixmap> *avatarCache,
const Style &style)
const QMap<QString, QPixmap> *avatarCache)
{
QPainterPath clipPath;
clipPath.addEllipse(avatarRect);
@ -231,7 +183,7 @@ void UserListPainter::drawAvatar(QPainter *painter,
}
if (!drewAvatar) {
painter->setBrush(blend(accentColor, style.base, style.dark ? 0.45 : 0.72));
painter->setBrush(accentColor.darker(200));
painter->setPen(Qt::NoPen);
painter->drawEllipse(avatarRect);
@ -244,9 +196,9 @@ void UserListPainter::drawAvatar(QPainter *painter,
painter->restore();
}
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style)
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online)
{
const QColor statusColor = online ? QColor(34, 197, 94) : style.ringOffline;
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95);
painter->setPen(QPen(statusColor, 2));
painter->setBrush(Qt::NoBrush);
@ -260,7 +212,7 @@ void UserListPainter::drawUserName(QPainter *painter,
int textX,
const QString &userName,
bool online,
const Style &style)
bool selected)
{
QFont nameFont = option.font;
nameFont.setBold(true);
@ -269,12 +221,10 @@ void UserListPainter::drawUserName(QPainter *painter,
const QRect nameRect(textX, rect.top() + 8, cardRight - textX - 10, 20);
const QString elidedName = QFontMetrics(nameFont).elidedText(userName, Qt::ElideRight, cardRight - textX - 10);
if (style.dropShadow) {
painter->setPen(QColor(0, 0, 0, 200));
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
}
painter->setPen(QColor(0, 0, 0, 200));
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
painter->setPen(online ? style.textOnline : style.textOffline);
painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115));
painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
}
@ -312,8 +262,7 @@ void UserListPainter::drawBadges(QPainter *painter,
const QRect &rect,
int cardRight,
const QList<Badge> &badges,
bool online,
const Style &style)
bool online)
{
if (badges.isEmpty()) {
return;
@ -335,17 +284,15 @@ void UserListPainter::drawBadges(QPainter *painter,
int bx = cardRight - 6 - totalBadgeW;
for (const Badge &b : badges) {
const QColor col = online ? b.color : blend(b.color, style.base, 0.55);
const QColor surface = blend(col, style.base, style.dark ? 0.55 : 0.78);
const QColor text = style.dark ? blend(col, Qt::white, 0.5) : blend(col, Qt::black, 0.35);
const 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(surface);
painter->setBrush(col.darker(online ? 160 : 220));
painter->drawRoundedRect(br, 3, 3);
painter->setPen(text);
painter->setPen(col.lighter(online ? 160 : 100));
painter->drawText(br, Qt::AlignCenter, b.text);
bx += bw + 4;
@ -358,19 +305,11 @@ void UserListPainter::paint(QPainter *painter,
const ServerInfo_User &userInfo,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap,
bool dark)
const QMap<QString, CardArtParams> *cardArtParamsMap)
{
painter->save();
painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
// The delegate supplies the application palette in option.palette, which
// always reflects the active theme. The widget palette can be stale after
// a runtime theme change, so it is only used as a defensive fallback.
const QPalette pal =
option.palette == QPalette() ? (option.widget ? option.widget->palette() : qApp->palette()) : option.palette;
const Style style = resolveStyle(pal, dark);
const QRect rect = option.rect;
const bool online = index.data(Qt::UserRole + 1).toBool();
const bool selected = option.state & QStyle::State_Selected;
@ -378,9 +317,6 @@ void UserListPainter::paint(QPainter *painter,
const QString userName = QString::fromStdString(userInfo.name());
const QString privLevel = QString::fromStdString(userInfo.privlevel());
const QColor accentColor = getAccentColor(userLevel, online);
const bool hasRole = userLevel.testFlag(ServerInfo_User::IsAdmin) ||
userLevel.testFlag(ServerInfo_User::IsModerator) ||
userLevel.testFlag(ServerInfo_User::IsJudge);
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
const int cardRight = getCardRight(option, rect);
@ -388,19 +324,19 @@ void UserListPainter::paint(QPainter *painter,
? cardArtParamsMap->value(userName)
: CardArtParams{};
drawBackground(painter, cardRect, accentColor, selected, style, hasRole);
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, style);
drawStatusRing(painter, avatarRect, online, style);
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, style);
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, style);
drawBadges(painter, option, rect, cardRight, badges, online);
painter->restore();
}

View file

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

View file

@ -1,88 +0,0 @@
#include "user_list_panel_widget.h"
#include "../../../../client/settings/cache_settings.h"
#include "user_list_manager.h"
#include "user_list_widget.h"
#include <QLineEdit>
#include <QVBoxLayout>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/settings/interface_settings.h>
namespace
{
// The persisted section keys are the serialization contract with the user's
// settings file, so the values must stay stable across versions.
QString sectionKey(UserListWidget::Section section)
{
switch (section) {
case UserListWidget::Section::Buddy:
return QStringLiteral("buddy");
case UserListWidget::Section::Online:
return QStringLiteral("online");
case UserListWidget::Section::Ignore:
return QStringLiteral("ignore");
}
return {};
}
} // namespace
UserListPanelWidget::UserListPanelWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
: QWidget(parent)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(2);
searchBar = new QLineEdit(this);
searchBar->setClearButtonEnabled(true);
mainLayout->addWidget(searchBar);
userList = new UserListWidget(_tabSupervisor, _client, UserListWidget::RoomList, this);
userList->setSectioned(
{UserListWidget::Section::Buddy, UserListWidget::Section::Online, UserListWidget::Section::Ignore});
mainLayout->addWidget(userList, 1);
connect(searchBar, &QLineEdit::textChanged, userList, &UserListWidget::setFilterText);
connect(userList, &UserListWidget::sectionExpanded, this, &UserListPanelWidget::persistExpandedSections);
connect(userList, &UserListWidget::openMessageDialog, this, &UserListPanelWidget::openMessageDialog);
// Restore the persisted expansion state, then apply it to the tree.
const QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
for (const UserListWidget::Section section : userList->getSectionIds()) {
userList->setSectionExpanded(section, expandedSections.contains(sectionKey(section)));
}
retranslateUi();
}
void UserListPanelWidget::bind(UserListManager *manager)
{
userList->bind(manager);
}
void UserListPanelWidget::persistExpandedSections(UserListWidget::Section section, bool expanded)
{
const QString key = sectionKey(section);
QStringList expandedSections = SettingsCache::instance().userInterface().getUserListExpandedSections();
if (expanded) {
if (!expandedSections.contains(key)) {
expandedSections.append(key);
}
} else {
expandedSections.removeAll(key);
}
SettingsCache::instance().userInterface().setUserListExpandedSections(expandedSections);
}
void UserListPanelWidget::retranslateUi()
{
searchBar->setPlaceholderText(tr("Search users..."));
userList->retranslateUi();
}
UserListWidget *UserListPanelWidget::getUserList() const
{
return userList;
}

View file

@ -1,43 +0,0 @@
/**
* @file user_list_panel_widget.h
* @ingroup Lobby
*/
#ifndef COCKATRICE_USER_LIST_PANEL_WIDGET_H
#define COCKATRICE_USER_LIST_PANEL_WIDGET_H
#include "user_list_widget.h"
#include <QWidget>
class AbstractClient;
class QLineEdit;
class TabSupervisor;
class UserListManager;
/**
* A unified user list: a search bar above a single tree whose section headers
* (buddy, online, ignored) are inline dividers. The tree owns the scrolling.
*/
class UserListPanelWidget : public QWidget
{
Q_OBJECT
public:
explicit UserListPanelWidget(TabSupervisor *tabSupervisor, AbstractClient *client, QWidget *parent = nullptr);
void bind(UserListManager *manager);
void retranslateUi();
[[nodiscard]] UserListWidget *getUserList() const;
signals:
void openMessageDialog(const QString &userName, bool focus);
private:
void persistExpandedSections(UserListWidget::Section section, bool expanded);
QLineEdit *searchBar = nullptr;
UserListWidget *userList = nullptr;
};
#endif // COCKATRICE_USER_LIST_PANEL_WIDGET_H

View file

@ -18,7 +18,6 @@
#include <QDialog>
#include <QGroupBox>
#include <QQueue>
#include <QSet>
#include <QStyledItemDelegate>
#include <QTextEdit>
#include <QTreeWidgetItem>
@ -104,13 +103,12 @@ public:
class UserListItemDelegate : public QStyledItemDelegate
{
QTreeWidget *tree;
const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *cardArtParamsMap;
public:
explicit UserListItemDelegate(QTreeWidget *tree,
explicit UserListItemDelegate(QObject *const parent,
const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap);
@ -149,12 +147,6 @@ public:
BuddyList,
IgnoreList
};
enum class Section
{
Buddy,
Online,
Ignore
};
private:
UserListManager *manager = nullptr;
@ -162,69 +154,30 @@ private:
UserCardArtProvider *cardArtProvider = nullptr;
QMap<QString, CardArtParams> cardArtParamsMap;
// ── Hover popup ───────────────────────────────────────────────────────────
UserInfoPopup *userInfoPopup = nullptr;
QTimer *showPopupTimer = nullptr;
QTimer *hidePopupTimer = nullptr;
QString hoveredUser;
bool popupPinned = false;
bool bulkLoading = false;
UserInfoPopup *m_userInfoPopup = nullptr;
QTimer *m_showPopupTimer = nullptr;
QTimer *m_hidePopupTimer = nullptr;
QString m_hoveredUser;
bool m_popupPinned = false;
bool m_bulkLoading = false;
/**
* Popup functions are anchored on the row, not the user name. In sectioned
* mode a user can own several rows (online + buddy), and the popup must
* follow the hovered/selected row rather than a lookup by name.
*/
void showPopupForUser(UserListTWI *item);
void showPopupForUser(const QString &userName);
void hidePopup(bool immediate = false);
void positionPopup(UserListTWI *item);
void positionPopup(const QString &userName);
void connectPopupSignals();
/** True when @p widget is the tree, the popup or an open menu. */
bool isPressInsideListUi(const QWidget *widget) const;
void clearSelectionAndClosePopup();
bool isItemNearViewport(const UserListTWI *item) const;
void requestAvatarsForVisibleItems();
// Sectioned mode (single tree with inline dividers)
bool sectioned = false;
QList<Section> sectionIds;
QMap<Section, QTreeWidgetItem *> sectionItems;
// One row per (section, user): a user that is online AND a buddy appears in
// both the "Online" and the "Buddies" sections, so the same user can own
// several rows, each hanging off its section's divider.
QMap<Section, QMap<QString, UserListTWI *>> sectionUsers;
QSet<Section> expandedSections;
void createSectionItems();
QTreeWidgetItem *createSectionItem(Section section);
[[nodiscard]] QString sectionTitle(Section section) const;
void updateSectionDivider(Section section);
void handleSectionExpansion(QTreeWidgetItem *item, bool expanded);
void setExpandedProgrammatically(QTreeWidgetItem *item, bool expanded);
void handleOnlineChange(const ServerInfo_User &user);
void handleOnlineChangeLeft(const QString &userName);
void handleListAdd(Section section, const ServerInfo_User &user);
void handleListRemove(Section section, const QString &userName);
/** Creates or updates the row for @p user in @p section. */
UserListTWI *ensureSectionMembership(Section section, const ServerInfo_User &user, bool online);
/** Removes and deletes the row for @p userName in @p section. */
bool dropSectionMembership(Section section, const QString &userName);
/** Sorts, refilters and repaints after a sectioned mode mutation. */
void finishSectionedMutation();
void updateCardArtParams(const ServerInfo_User &user, const QString &userName);
void processUserInfo(Section section, const ServerInfo_User &user, bool online);
QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor;
AbstractClient *client;
UserListType type;
QTreeWidget *userTree = nullptr;
QTreeWidget *userTree;
UserListItemDelegate *itemDelegate;
UserContextMenu *userContextMenu;
int onlineCount;
QString titleStr;
QString filterText;
bool showTitle = true;
void updateCount();
void applyFilter();
void refreshPopupButtons(const QString &userName);
private slots:
void userClicked(QTreeWidgetItem *item, int column);
@ -236,14 +189,12 @@ signals:
void addIgnore(const QString &userName);
void removeIgnore(const QString &userName);
void joinGameRequested(int gameId, int roomId, bool asSpectator);
void sectionExpanded(Section section, bool expanded);
public:
UserListWidget(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
UserListType _type,
QWidget *parent = nullptr);
~UserListWidget() override;
void bind(UserListManager *mgr);
void applyDisplayMode();
void beginBulkLoad();
@ -254,14 +205,6 @@ public:
void processUserInfo(const ServerInfo_User &user, bool online);
bool deleteUser(const QString &userName);
void setUserOnline(const QString &userName, bool online);
void setFilterText(const QString &text);
void setShowTitle(bool showTitle);
void setSectioned(const QList<Section> &ids);
void setSectionExpanded(Section section, bool expanded);
[[nodiscard]] const QList<Section> &getSectionIds() const
{
return sectionIds;
}
[[nodiscard]] const QMap<QString, UserListTWI *> &getUsers() const
{
return users;

View file

@ -2,7 +2,6 @@
#include "../../../client/settings/cache_settings.h"
#include "../main.h"
#include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h"
#include <QCoreApplication>
@ -12,7 +11,6 @@
#include <QTranslator>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h>
#include <libcockatrice/utility/macros.h>
@ -123,61 +121,8 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&showTipsOnStartup, &QCheckBox::clicked, &settings.personal(), &PersonalSettings::setShowTipsOnStartup);
// startup destination
for (int i = 0; i < 8; ++i) {
startupTabSelector.addItem(""); // texts set in retranslateUi
}
startupTabSelector.setCurrentIndex(settings.tabs().getStartupTabIndex());
connect(&startupTabSelector, qOverload<int>(&QComboBox::currentIndexChanged), &settings.tabs(),
&TabsSettings::setStartupTabIndex);
connect(&startupTabSelector, qOverload<int>(&QComboBox::currentIndexChanged), this,
&GeneralSettingsPage::updateStartupServerControlsVisibility);
const QString savedHost = settings.tabs().getStartupServerHost();
const QString savedPort = settings.tabs().getStartupServerPort();
int startupServerIndex = -1;
UserConnection_Information uci;
for (const auto &savedServer : uci.getServerInfo()) {
const UserConnection_Information &info = savedServer.second;
const QString saveName = info.getSaveName();
if (saveName.isEmpty()) {
continue;
}
startupServerSelector.addItem(saveName, QVariantList{info.getServer(), info.getPort()});
if (startupServerIndex == -1 && info.getServer() == savedHost && info.getPort() == savedPort) {
startupServerIndex = startupServerSelector.count() - 1;
}
}
startupServerSelector.setCurrentIndex(startupServerIndex);
connect(&startupServerSelector, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
const QVariantList serverInfo = startupServerSelector.itemData(index).toList();
if (serverInfo.size() != 2) {
return;
}
TabsSettings &tabs = SettingsCache::instance().tabs();
tabs.setStartupServerHost(serverInfo[0].toString());
tabs.setStartupServerPort(serverInfo[1].toString());
});
startupRoomNameEdit = new QLineEdit(settings.tabs().getStartupRoomName());
// Default (Expanding) would stretch the whole controls column when this row becomes visible,
// so size it like the combo boxes instead: fills the column, never widens it.
startupRoomNameEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
connect(startupRoomNameEdit, &QLineEdit::editingFinished, this,
[this] { SettingsCache::instance().tabs().setStartupRoomName(startupRoomNameEdit->text().trimmed()); });
auto *startupGrid = new QGridLayout;
startupGrid->addWidget(&showTipsOnStartup, 0, 0, 1, 2);
startupGrid->addWidget(&startupTabLabel, 1, 0);
startupGrid->addWidget(&startupTabSelector, 1, 1);
startupGrid->addWidget(&startupServerLabel, 2, 0);
startupGrid->addWidget(&startupServerSelector, 2, 1);
startupGrid->addWidget(&startupRoomLabel, 3, 0);
startupGrid->addWidget(startupRoomNameEdit, 3, 1);
updateStartupServerControlsVisibility();
startupGroupBox = new QGroupBox;
startupGroupBox->setLayout(startupGrid);
@ -412,17 +357,6 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString());
}
void GeneralSettingsPage::updateStartupServerControlsVisibility()
{
const int index = startupTabSelector.currentIndex();
const bool serverNeeded = index == StartupTab::StartupTabServer || index == StartupTab::StartupTabServerRoom;
const bool roomNeeded = index == StartupTab::StartupTabServerRoom;
startupServerLabel.setVisible(serverNeeded);
startupServerSelector.setVisible(serverNeeded);
startupRoomLabel.setVisible(roomNeeded);
startupRoomNameEdit->setVisible(roomNeeded);
}
void GeneralSettingsPage::retranslateUi()
{
languageGroupBox->setTitle(tr("Language settings"));
@ -459,20 +393,6 @@ void GeneralSettingsPage::retranslateUi()
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
showTipsOnStartup.setText(tr("Show tips on startup"));
startupTabLabel.setText(tr("Startup tab:"));
startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home"));
startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckStorage, tr("Visual Deck Storage"));
startupTabSelector.setItemText(StartupTab::StartupTabDeckStorage, tr("Deck Storage"));
startupTabSelector.setItemText(StartupTab::StartupTabReplays, tr("Game Replays"));
startupTabSelector.setItemText(StartupTab::StartupTabDeckEditor, tr("Deck Editor"));
startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckEditor, tr("Visual Deck Editor"));
startupTabSelector.setItemText(StartupTab::StartupTabServer, tr("Server"));
startupTabSelector.setItemText(StartupTab::StartupTabServerRoom, tr("Server Room"));
startupTabSelector.setToolTip(
tr("The tab shown when Cockatrice starts. If the chosen tab is not open yet, it is opened."));
startupServerLabel.setText(tr("Server:"));
startupRoomLabel.setText(tr("Room:"));
startupRoomNameEdit->setPlaceholderText(tr("Room name"));
resetAllPathsButton->setText(tr("Reset all paths"));
const auto &settings = SettingsCache::instance();

View file

@ -30,7 +30,6 @@ private slots:
void tokenDatabasePathButtonClicked();
void resetAllPathsClicked();
void languageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private:
QStringList findQmFiles();
@ -72,12 +71,6 @@ private:
QLabel updateReleaseChannelLabel;
QLabel advertiseTranslationPageLabel;
QCheckBox showTipsOnStartup;
QLabel startupTabLabel;
QComboBox startupTabSelector;
QLabel startupServerLabel;
QComboBox startupServerSelector;
QLabel startupRoomLabel;
QLineEdit *startupRoomNameEdit;
};
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H

View file

@ -183,13 +183,6 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&defaultDeckEditorTypeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged),
&SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType);
vdeStartupTabSelector.addItem(""); // these will be set in retranslateUI
vdeStartupTabSelector.addItem("");
vdeStartupTabSelector.addItem("");
vdeStartupTabSelector.setCurrentIndex(SettingsCache::instance().deckEditor().getVdeStartupTab());
connect(&vdeStartupTabSelector, QOverload<int>::of(&QComboBox::currentIndexChanged),
&SettingsCache::instance().deckEditor(), &DeckEditorSettings::setVdeStartupTab);
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?");
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true);
commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false);
@ -249,12 +242,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
deckEditorGrid->addWidget(&vdeStartupTabLabel, 5, 0);
deckEditorGrid->addWidget(&vdeStartupTabSelector, 5, 1);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 6, 0);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 6, 1);
deckEditorGrid->addWidget(labelWidget, 7, 0);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 7, 1);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1);
deckEditorGrid->addWidget(labelWidget, 6, 0);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1);
deckEditorGroupBox = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid);
@ -377,12 +368,6 @@ void UserInterfaceSettingsPage::retranslateUi()
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
vdeStartupTabLabel.setText(tr("Visual deck editor startup tab"));
vdeStartupTabSelector.setItemText(VdeStartupTabContext, tr("Context"));
vdeStartupTabSelector.setItemText(VdeStartupTabDeckDisplay, tr("Deck display"));
vdeStartupTabSelector.setItemText(VdeStartupTabDatabaseDisplay, tr("Database display"));
vdeStartupTabSelector.setToolTip(
tr("Context mode: New decks open on the database display, existing decks open on the deck view."));
commanderSpellbookIntegrationEnabledLabel.setText(
tr("CommanderSpellbook integration to estimate commander bracket"));

View file

@ -50,8 +50,6 @@ private:
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
QLabel defaultDeckEditorTypeLabel;
QComboBox defaultDeckEditorTypeSelector;
QLabel vdeStartupTabLabel;
QComboBox vdeStartupTabSelector;
QLabel commanderSpellbookIntegrationEnabledLabel;
QComboBox commanderSpellbookIntegrationEnabledSelector;
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;

View file

@ -263,12 +263,12 @@ protected slots:
/** @brief Handles dock close events. */
void closeEvent(QCloseEvent *event) override;
private:
/** @brief Sets the deck for this tab.
* @param _deck The deck object.
*/
virtual void setDeck(const LoadedDeck &_deck);
private:
/** @brief Helper for editing decks from the clipboard. */
void editDeckInClipboard(bool annotated);

View file

@ -6,7 +6,6 @@
#include "../interface/widgets/server/chat_view/chat_view.h"
#include "../interface/widgets/server/game_selector.h"
#include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/server/user/user_list_panel_widget.h"
#include "../interface/widgets/server/user/user_list_widget.h"
#include "../main.h"
#include "../utility/completer_utils.h"
@ -61,10 +60,23 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
tempMap.insert(info.room_id(), gameTypes);
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
userListPanel = new UserListPanelWidget(tabSupervisor, client, this);
userListPanel->bind(tabSupervisor->getUserListManager());
userList = userListPanel->getUserList();
connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog);
auto *tabs = new QTabWidget(this);
friendsList = new UserListWidget(tabSupervisor, client, UserListWidget::BuddyList);
friendsList->bind(tabSupervisor->getUserListManager());
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
userList->bind(tabSupervisor->getUserListManager());
ignoreList = new UserListWidget(tabSupervisor, client, UserListWidget::IgnoreList);
ignoreList->bind(tabSupervisor->getUserListManager());
connect(friendsList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
tabs->addTab(friendsList, tr("Friends"));
tabs->addTab(userList, tr("Online"));
tabs->addTab(ignoreList, tr("Ignored"));
chatView = new ChatView(tabSupervisor, nullptr, true, this);
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
@ -114,7 +126,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
auto *hbox = new QHBoxLayout;
hbox->addWidget(splitter, 3);
hbox->addWidget(userListPanel, 1);
hbox->addWidget(tabs, 1);
aLeaveRoom = new QAction(this);
connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest);
@ -169,7 +181,7 @@ void TabRoom::retranslateUi()
{
gameSelector->retranslateUi();
chatView->retranslateUi();
userListPanel->retranslateUi();
userList->retranslateUi();
sayLabel->setText(tr("&Say:"));
chatGroupBox->setTitle(tr("Chat"));
roomMenu->setTitle(tr("&Room"));

View file

@ -27,7 +27,6 @@ class Message;
} // namespace google
class AbstractClient;
class UserListWidget;
class UserListPanelWidget;
class QLabel;
class ChatView;
class QPushButton;
@ -58,8 +57,9 @@ private:
QMap<int, QString> gameTypes;
GameSelector *gameSelector;
UserListPanelWidget *userListPanel;
UserListWidget *friendsList;
UserListWidget *userList;
UserListWidget *ignoreList;
const UserListProxy *userListProxy;
ChatView *chatView;
QLabel *sayLabel;
@ -114,10 +114,6 @@ public:
{
return roomId;
}
[[nodiscard]] QString getRoomName() const
{
return roomName;
}
[[nodiscard]] const QMap<int, QString> &getGameTypes() const
{
return gameTypes;

View file

@ -335,12 +335,7 @@ static void checkAndTrigger(QAction *checkableAction, bool checked)
}
/**
* Opens the always-available tabs, depending on settings, and lands on the configured startup tab.
*
* The startup destination is a request: tabs that were not open before (deck editors, storage
* tabs disabled in the Tabs menu) are opened as part of the startup flow. Destinations that
* require a server connection (Server, Server Room) are handled asynchronously by MainWindow
* through the intent system, since this class has no RemoteClient.
* Opens the always-available tabs, depending on settings.
*/
void TabSupervisor::initStartupTabs()
{
@ -356,42 +351,6 @@ void TabSupervisor::initStartupTabs()
if (SettingsCache::instance().tabs().getTabReplaysOpen()) {
openTabReplays();
}
switch (SettingsCache::instance().tabs().getStartupTabIndex()) {
case StartupTab::StartupTabVisualDeckStorage:
if (!tabVisualDeckStorage) {
openTabVisualDeckStorage();
}
setCurrentWidget(tabVisualDeckStorage);
break;
case StartupTab::StartupTabDeckStorage:
if (!tabDeckStorage) {
openTabDeckStorage();
}
setCurrentWidget(tabDeckStorage);
break;
case StartupTab::StartupTabReplays:
if (!tabReplays) {
openTabReplays();
}
setCurrentWidget(tabReplays);
break;
case StartupTab::StartupTabDeckEditor:
addDeckEditorTab(LoadedDeck());
break;
case StartupTab::StartupTabVisualDeckEditor:
addVisualDeckEditorTab(LoadedDeck());
break;
case StartupTab::StartupTabServer:
case StartupTab::StartupTabServerRoom:
// Handled asynchronously by MainWindow::applyStartupDestination(); Home stays selected
// until the server connection succeeds.
break;
case StartupTab::StartupTabHome:
default:
setCurrentWidget(tabHome);
break;
}
}
/**

View file

@ -184,7 +184,6 @@ public slots:
void actTabVisualDeckStorage(bool checked);
void actTabReplays(bool checked);
void openTabServer();
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
private slots:
void refreshShortcuts();
@ -210,6 +209,7 @@ private slots:
void gameJoined(const Event_GameJoined &event);
void localGameJoined(const Event_GameJoined &event);
void gameLeft(TabGame *tab);
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
void roomLeft(TabRoom *tab);
TabMessage *addMessageTab(const QString &userName, bool focus);
void replayLeft(TabGame *tab);

View file

@ -30,7 +30,6 @@
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/layouts_settings.h>
/**
@ -99,33 +98,6 @@ void TabDeckEditorVisual::onDeckChanged()
tabContainer->sampleHandWidget->setDeckModel(deckStateManager->getModel());
}
/** @brief Sets the deck and selects the startup sub-tab matching the context. */
void TabDeckEditorVisual::setDeck(const LoadedDeck &_deck)
{
AbstractTabDeckEditor::setDeck(_deck);
int startupTab = SettingsCache::instance().deckEditor().getVdeStartupTab();
if (startupTab == VdeStartupTabContext) {
// New (empty) decks open on the database display so cards can be added
// right away. Existing decks open on the deck view.
startupTab = _deck.isEmpty() ? VdeStartupTabDatabaseDisplay : VdeStartupTabDeckDisplay;
}
switch (startupTab) {
case VdeStartupTabDatabaseDisplay:
tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDatabaseDisplay);
break;
case VdeStartupTabDeckDisplay:
tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDeckView);
break;
default:
qCWarning(TabSupervisorLog) << "Unknown VdeStartupTab [" << startupTab
<< "]; falling back to the deck view";
tabContainer->setCurrentIndex(TabDeckEditorVisualTabWidget::TabIndex::VisualDeckView);
break;
}
}
/** @brief Creates menus for deck editing and view options, including dock actions. */
void TabDeckEditorVisual::createMenus()
{

View file

@ -164,15 +164,6 @@ public slots:
* @return true if successful, false otherwise.
*/
bool actSaveDeckAs() override;
private:
/**
* @brief Sets the deck for this tab and selects the sub-tab to open on
* startup, per the "Visual deck editor startup tab" setting (Context /
* Deck display / Database display).
* @param _deck The deck object.
*/
void setDeck(const LoadedDeck &_deck) override;
};
#endif

View file

@ -49,17 +49,6 @@ class TabDeckEditorVisualTabWidget : public QTabWidget
Q_OBJECT
public:
/**
* @brief Sub-tab order in the container; addNewTab() is called in this order.
*/
enum TabIndex
{
VisualDeckView,
VisualDatabaseDisplay,
DeckAnalytics,
SampleHand,
};
/**
* @brief Construct the tab widget with required models.
* @param parent Parent widget.

View file

@ -32,14 +32,8 @@
#include "../interface/widgets/dialogs/dlg_update.h"
#include "../interface/widgets/dialogs/dlg_view_log.h"
#include "../interface/widgets/tabs/tab_game.h"
#include "../interface/widgets/tabs/tab_server.h"
#include "../interface/widgets/tabs/tab_supervisor.h"
#include "../main.h"
#include "intents/contexts/context_connect_to_server.h"
#include "intents/contexts/context_join_room.h"
#include "intents/intent_connect_to_server.h"
#include "intents/intent_login.h"
#include "intents/intent_open_server_room_by_name.h"
#include "logger.h"
#include "version_string.h"
#include "widgets/dialogs/dlg_connect.h"
@ -83,7 +77,6 @@
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h>
#define GITHUB_PAGES_URL "https://cockatrice.github.io"
@ -547,7 +540,6 @@ MainWindow::MainWindow(QWidget *parent)
// run startup check async
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
}
void MainWindow::startupConfigCheck()
@ -656,82 +648,6 @@ void MainWindow::startupConfigCheck()
}
}
/**
* Drives the server-based startup destinations (Server lobby, Server Room) through the intent
* system: fetch saved credentials, connect to the configured server, then land on the Lobby or
* join the configured room by name.
*/
void MainWindow::applyStartupDestination()
{
// An explicit command-line connect takes precedence over the startup destination.
if (!connectTo.isEmpty()) {
return;
}
const int destination = SettingsCache::instance().tabs().getStartupTabIndex();
if (destination != StartupTab::StartupTabServer && destination != StartupTab::StartupTabServerRoom) {
return;
}
const QString host = SettingsCache::instance().tabs().getStartupServerHost();
const QString port = SettingsCache::instance().tabs().getStartupServerPort();
if (host.isEmpty() || port.isEmpty()) {
qCWarning(WindowMainStartupLog) << "Startup destination needs a configured server";
return;
}
auto serverContext = std::make_shared<ContextConnectToServer>();
serverContext->hostname = host;
serverContext->port = port;
auto *credentials = new IntentGetLoginCredentials(serverContext.get());
auto *connector = new IntentConnectToServer(getRemoteClient(), serverContext.get());
connect(credentials, &Intent::finished, connector, &Intent::execute);
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
connect(connector, &Intent::finished, this,
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
credentials->execute();
}
void MainWindow::onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext)
{
// The server tab must exist: it is what requests the room list.
if (!tabSupervisor->getTabServer()) {
tabSupervisor->openTabServer();
}
if (destination == StartupTab::StartupTabServerRoom) {
auto roomContext = std::make_unique<ContextJoinRoom>();
roomContext->serverContext = serverContext;
auto *roomIntent = new IntentOpenServerRoomByName(tabSupervisor, getRemoteClient(), std::move(roomContext),
SettingsCache::instance().tabs().getStartupRoomName());
roomIntent->setParent(this);
connect(roomIntent, &Intent::failed, this, &MainWindow::startupDestinationFailed);
roomIntent->execute();
return;
}
if (tabSupervisor->getTabServer()) {
tabSupervisor->setCurrentWidget(tabSupervisor->getTabServer());
} else {
qCWarning(WindowMainStartupLog) << "Startup destination: server tab could not be opened";
}
}
void MainWindow::startupDestinationFailed(const QString &reason)
{
qCWarning(WindowMainStartupLog) << "Startup destination failed:" << reason;
}
bool MainWindow::startupDestinationConnectsToServer() const
{
const int destination = SettingsCache::instance().tabs().getStartupTabIndex();
return destination == StartupTab::StartupTabServer || destination == StartupTab::StartupTabServerRoom;
}
void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
{
if (isUpdate) {
@ -834,8 +750,7 @@ void MainWindow::changeEvent(QEvent *event)
connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
connectTo.password());
} else if (SettingsCache::instance().servers().getAutoConnect() &&
!SettingsCache::instance().debug().getLocalGameOnStartup() &&
!startupDestinationConnectsToServer()) {
!SettingsCache::instance().debug().getLocalGameOnStartup()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
DlgConnect dlg(this);
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),

View file

@ -55,7 +55,6 @@ class ServerInfo_User;
class TabSupervisor;
class WndSets;
class DlgTipOfTheDay;
struct ContextConnectToServer;
class MainWindow : public QMainWindow
{
@ -106,11 +105,6 @@ private slots:
void startupConfigCheck();
void alertForcedOracleRun(const QString &version, bool isUpdate);
void applyStartupDestination();
void onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext);
void startupDestinationFailed(const QString &reason);
[[nodiscard]] bool startupDestinationConnectsToServer() const;
private:
static const QString appName;
static const QStringList fileNameFilters;

View file

@ -2,7 +2,6 @@
#define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
#include <QString>
#include <QStringList>
class IInterfaceSettingsProvider
{
@ -42,7 +41,6 @@ public:
[[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0;
[[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0;
[[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0;
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
};
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H

View file

@ -1,17 +1,11 @@
#ifndef COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
#define COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
#include <QString>
class ITabsSettingsProvider
{
public:
virtual ~ITabsSettingsProvider() = default;
[[nodiscard]] virtual int getStartupTabIndex() const = 0;
[[nodiscard]] virtual QString getStartupServerHost() const = 0;
[[nodiscard]] virtual QString getStartupServerPort() const = 0;
[[nodiscard]] virtual QString getStartupRoomName() const = 0;
[[nodiscard]] virtual bool getTabVisualDeckStorageOpen() const = 0;
[[nodiscard]] virtual bool getTabServerOpen() const = 0;
[[nodiscard]] virtual bool getTabAccountOpen() const = 0;

View file

@ -25,11 +25,6 @@ int DeckEditorSettings::getDefaultDeckEditorType() const
return getValue("defaultDeckEditorType", QString(), QString(), 1).toInt();
}
int DeckEditorSettings::getVdeStartupTab() const
{
return getValue("vdeStartupTab", QString(), QString(), VdeStartupTabContext).toInt();
}
void DeckEditorSettings::setOpenDeckInNewTab(bool _openDeckInNewTab)
{
setValue(_openDeckInNewTab, "openDeckInNewTab");
@ -52,12 +47,6 @@ void DeckEditorSettings::setDefaultDeckEditorType(int _defaultDeckEditorType)
setValue(_defaultDeckEditorType, "defaultDeckEditorType");
}
void DeckEditorSettings::setVdeStartupTab(int _vdeStartupTab)
{
setValue(_vdeStartupTab, "vdeStartupTab");
emit vdeStartupTabChanged(_vdeStartupTab);
}
int DeckEditorSettings::getCommanderSpellbookIntegrationEnabled() const
{
return getValue("commanderspellbookintegrationenabled", QString(), QString(),

View file

@ -13,13 +13,6 @@ enum commanderSpellbookIntegrationEnabledIndex
commanderSpellbookIntegrationEnabledIndexUnprompted,
};
enum VdeStartupTab
{
VdeStartupTabContext, ///< Match the opened deck: new decks show the database display, existing decks the deck view
VdeStartupTabDeckDisplay, ///< Always open the Visual Deck View
VdeStartupTabDatabaseDisplay, ///< Always open the Visual Database Display
};
class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider
{
Q_OBJECT
@ -30,7 +23,6 @@ public:
[[nodiscard]] bool getBannerCardComboBoxVisible() const override;
[[nodiscard]] bool getTagsWidgetVisible() const override;
[[nodiscard]] int getDefaultDeckEditorType() const override;
[[nodiscard]] int getVdeStartupTab() const;
[[nodiscard]] int getCommanderSpellbookIntegrationEnabled() const;
[[nodiscard]] bool getCommanderSpellbookIntegrationUseOfficialBracketNames() const;
@ -38,7 +30,6 @@ public:
void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible);
void setTagsWidgetVisible(bool _tagsWidgetVisible);
void setDefaultDeckEditorType(int _defaultDeckEditorType);
void setVdeStartupTab(int _vdeStartupTab);
void setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled);
void setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames);
@ -47,7 +38,6 @@ signals:
void tagsWidgetVisibleChanged(bool visible);
void commanderSpellbookIntegrationEnabledChanged(int enabled);
void commanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool useOfficialBracketNames);
void vdeStartupTabChanged(int vdeStartupTab);
public:
explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -170,12 +170,6 @@ bool InterfaceSettings::getBattlefieldFlashEnabled() const
return getValue("battlefieldFlashEnabled", QString(), QString(), true).toBool();
}
QStringList InterfaceSettings::getUserListExpandedSections() const
{
return getValue("userListExpandedSections", QString(), QString(), QStringList({"buddy", "online", "ignore"}))
.toStringList();
}
void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus)
{
setValue(_useTearOffMenus, "useTearOffMenus");
@ -354,8 +348,3 @@ void InterfaceSettings::setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled
setValue(_battlefieldFlashEnabled, "battlefieldFlashEnabled");
emit battlefieldFlashEnabledChanged(_battlefieldFlashEnabled);
}
void InterfaceSettings::setUserListExpandedSections(const QStringList &sections)
{
setValue(sections, "userListExpandedSections");
}

View file

@ -44,7 +44,6 @@ public:
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
[[nodiscard]] bool getLifeCounterAnimationsEnabled() const override;
[[nodiscard]] bool getBattlefieldFlashEnabled() const override;
[[nodiscard]] QStringList getUserListExpandedSections() const override;
void setUseTearOffMenus(bool _useTearOffMenus);
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
@ -79,7 +78,6 @@ public:
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled);
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
void setUserListExpandedSections(const QStringList &sections);
signals:
void useTearOffMenusChanged(bool state);

View file

@ -5,26 +5,6 @@ TabsSettings::TabsSettings(const QString &settingPath, QObject *parent)
{
}
int TabsSettings::getStartupTabIndex() const
{
return getValue("startupTab", QString(), QString(), StartupTab::StartupTabHome).toInt();
}
QString TabsSettings::getStartupServerHost() const
{
return getValue("startupServerHost", QString(), QString(), QString()).toString();
}
QString TabsSettings::getStartupServerPort() const
{
return getValue("startupServerPort", QString(), QString(), QString()).toString();
}
QString TabsSettings::getStartupRoomName() const
{
return getValue("startupRoomName", QString(), QString(), QString()).toString();
}
bool TabsSettings::getTabVisualDeckStorageOpen() const
{
return getValue("visualDeckStorage", QString(), QString(), true).toBool();
@ -60,42 +40,6 @@ bool TabsSettings::getTabLogOpen() const
return getValue("log", QString(), QString(), true).toBool();
}
void TabsSettings::setStartupTabIndex(int value)
{
if (getStartupTabIndex() == value) {
return;
}
setValue(value, "startupTab");
emit startupTabIndexChanged(value);
}
void TabsSettings::setStartupServerHost(const QString &host)
{
if (getStartupServerHost() == host) {
return;
}
setValue(host, "startupServerHost");
emit startupServerHostChanged(host);
}
void TabsSettings::setStartupServerPort(const QString &port)
{
if (getStartupServerPort() == port) {
return;
}
setValue(port, "startupServerPort");
emit startupServerPortChanged(port);
}
void TabsSettings::setStartupRoomName(const QString &roomName)
{
if (getStartupRoomName() == roomName) {
return;
}
setValue(roomName, "startupRoomName");
emit startupRoomNameChanged(roomName);
}
void TabsSettings::setTabVisualDeckStorageOpen(bool value)
{
setValue(value, "visualDeckStorage");

View file

@ -5,35 +5,12 @@
#include <libcockatrice/interfaces/interface_tabs_settings_provider.h>
/**
* @brief The tab the application selects after launch.
*
* The destination is a request: tabs that were not open before (Deck Editor, Server Room, …)
* are opened as part of the startup flow. Destinations that require a server connection use the
* intent system to satisfy their pre-conditions.
*/
enum StartupTab
{
StartupTabHome, ///< The Home tab
StartupTabVisualDeckStorage, ///< The visual deck storage tab
StartupTabDeckStorage, ///< The deck storage tab
StartupTabReplays, ///< The game replays tab
StartupTabDeckEditor, ///< A fresh classic deck editor tab
StartupTabVisualDeckEditor, ///< A fresh visual deck editor tab
StartupTabServer, ///< The server lobby: connect and select the server tab
StartupTabServerRoom ///< A server room: connect and join the room by name
};
class TabsSettings : public SettingsManager, public ITabsSettingsProvider
{
Q_OBJECT
friend class SettingsCache;
public:
[[nodiscard]] int getStartupTabIndex() const override;
[[nodiscard]] QString getStartupServerHost() const override;
[[nodiscard]] QString getStartupServerPort() const override;
[[nodiscard]] QString getStartupRoomName() const override;
[[nodiscard]] bool getTabVisualDeckStorageOpen() const override;
[[nodiscard]] bool getTabServerOpen() const override;
[[nodiscard]] bool getTabAccountOpen() const override;
@ -42,10 +19,6 @@ public:
[[nodiscard]] bool getTabAdminOpen() const override;
[[nodiscard]] bool getTabLogOpen() const override;
void setStartupTabIndex(int value);
void setStartupServerHost(const QString &host);
void setStartupServerPort(const QString &port);
void setStartupRoomName(const QString &roomName);
void setTabVisualDeckStorageOpen(bool value);
void setTabServerOpen(bool value);
void setTabAccountOpen(bool value);
@ -54,12 +27,6 @@ public:
void setTabAdminOpen(bool value);
void setTabLogOpen(bool value);
signals:
void startupTabIndexChanged(int index);
void startupServerHostChanged(const QString &host);
void startupServerPortChanged(const QString &port);
void startupRoomNameChanged(const QString &roomName);
public:
explicit TabsSettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -188,38 +188,6 @@ TEST_F(SettingsDefaultsTest, Sound_MasterVolume_SetAndGet)
// --- TabsSettings ---
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_Default)
{
TabsSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(StartupTab::StartupTabHome));
}
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_SetAndGet)
{
TabsSettings s(settingsPath, nullptr);
s.setStartupTabIndex(StartupTab::StartupTabServerRoom);
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(StartupTab::StartupTabServerRoom));
}
TEST_F(SettingsDefaultsTest, Tabs_StartupServer_Default)
{
TabsSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getStartupServerHost(), QString());
ASSERT_EQ(s.getStartupServerPort(), QString());
ASSERT_EQ(s.getStartupRoomName(), QString());
}
TEST_F(SettingsDefaultsTest, Tabs_StartupServer_SetAndGet)
{
TabsSettings s(settingsPath, nullptr);
s.setStartupServerHost("server.cockatrice.us");
s.setStartupServerPort("4748");
s.setStartupRoomName("General");
ASSERT_EQ(s.getStartupServerHost(), QString("server.cockatrice.us"));
ASSERT_EQ(s.getStartupServerPort(), QString("4748"));
ASSERT_EQ(s.getStartupRoomName(), QString("General"));
}
TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default)
{
TabsSettings s(settingsPath, nullptr);
@ -432,23 +400,6 @@ TEST_F(SettingsDefaultsTest, DeckEditor_DefaultDeckEditorType_Default)
ASSERT_EQ(s.getDefaultDeckEditorType(), 1);
}
TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_Default)
{
DeckEditorSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext);
}
TEST_F(SettingsDefaultsTest, DeckEditor_VdeStartupTab_SetAndGet)
{
DeckEditorSettings s(settingsPath, nullptr);
s.setVdeStartupTab(VdeStartupTabDeckDisplay);
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDeckDisplay);
s.setVdeStartupTab(VdeStartupTabDatabaseDisplay);
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabDatabaseDisplay);
s.setVdeStartupTab(VdeStartupTabContext);
ASSERT_EQ(s.getVdeStartupTab(), VdeStartupTabContext);
}
// --- NetworkSettings ---
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)