mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-23 18:06:26 -07:00
Compare commits
3 commits
6ee1fd58e6
...
d99798111e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d99798111e | ||
|
|
16b6132701 | ||
|
|
fbe5c4ade0 |
36 changed files with 2305 additions and 431 deletions
|
|
@ -260,6 +260,7 @@ 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
|
||||
|
|
@ -396,6 +397,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
#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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#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
|
||||
|
|
@ -123,7 +123,7 @@ void ThemeManager::ensureThemeDirectoryExists()
|
|||
}
|
||||
}
|
||||
|
||||
bool ThemeManager::isDarkMode(const QString &themeDirPath)
|
||||
bool ThemeManager::isDarkMode(const QString &themeDirPath) const
|
||||
{
|
||||
ThemeConfig themeConfig = ThemeConfig::fromThemeDir(themeDirPath);
|
||||
if (themeConfig.colorScheme.compare("Dark", Qt::CaseInsensitive) == 0) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,14 @@ protected:
|
|||
|
||||
public:
|
||||
bool isBuiltInTheme();
|
||||
bool isDarkMode(const QString &themeDirPath);
|
||||
// Explicit color scheme of the theme: theme.cfg's ColorScheme setting
|
||||
// (Dark/Light), falling back to the OS color scheme when it is "System".
|
||||
bool isDarkMode(const QString &themeDirPath) const;
|
||||
// The resolved scheme of the currently active theme.
|
||||
bool isDarkModeActive() const
|
||||
{
|
||||
return isDarkMode(currentThemePath);
|
||||
}
|
||||
QStringMap &getAvailableThemes();
|
||||
// Returns the path to the currently active theme directory (empty = default)
|
||||
QString getCurrentThemePath() const
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#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"
|
||||
|
||||
|
|
@ -22,6 +23,42 @@
|
|||
#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
|
||||
|
|
@ -48,8 +85,14 @@ 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 ? QColor(35, 45, 62) : QColor(14, 18, 26));
|
||||
p->fillRect(rect, selected ? UserListPainter::blend(base, highlight, dark ? 0.45 : 0.30) : base);
|
||||
|
||||
// State colour dot
|
||||
const QColor dot = game.started() ? QColor(239, 68, 68)
|
||||
|
|
@ -64,7 +107,7 @@ public:
|
|||
QFont tf = option.font;
|
||||
tf.setBold(true);
|
||||
p->setFont(tf);
|
||||
p->setPen(QColor(205, 215, 230));
|
||||
p->setPen(pal.color(QPalette::Text));
|
||||
const int textX = rect.left() + 26;
|
||||
const int countW = 52;
|
||||
const int titleW = rect.width() - textX - countW - 6;
|
||||
|
|
@ -74,13 +117,15 @@ public:
|
|||
// Player count
|
||||
const bool full = game.player_count() >= game.max_players();
|
||||
p->setFont(option.font);
|
||||
p->setPen(full ? QColor(249, 115, 22) : QColor(110, 128, 150));
|
||||
p->setPen(full ? QColor(249, 115, 22) : pal.color(QPalette::Disabled, QPalette::Text));
|
||||
p->drawText(QRect(rect.right() - countW - 4, rect.top(), countW, rect.height()),
|
||||
Qt::AlignVCenter | Qt::AlignRight,
|
||||
QStringLiteral("%1/%2").arg(game.player_count()).arg(game.max_players()));
|
||||
|
||||
// Row separator
|
||||
p->setPen(QColor(24, 32, 44));
|
||||
QColor separator = pal.color(QPalette::Mid);
|
||||
separator.setAlpha(90);
|
||||
p->setPen(separator);
|
||||
p->drawLine(rect.bottomLeft(), rect.bottomRight());
|
||||
|
||||
p->restore();
|
||||
|
|
@ -95,17 +140,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 ¶ms)
|
||||
void UserInfoHeaderWidget::setUserData(const ServerInfo_User &_user,
|
||||
bool _online,
|
||||
const QPixmap &_avatar,
|
||||
const QPixmap &_cardArt,
|
||||
const CardArtParams &_params)
|
||||
{
|
||||
m_user = user;
|
||||
m_online = online;
|
||||
m_avatar = avatar;
|
||||
m_cardArt = cardArt;
|
||||
m_params = params;
|
||||
user = _user;
|
||||
online = _online;
|
||||
avatar = _avatar;
|
||||
cardArt = _cardArt;
|
||||
params = _params;
|
||||
update();
|
||||
}
|
||||
|
||||
|
|
@ -115,29 +160,37 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
const QRect rect = this->rect();
|
||||
const UserLevelFlags level(m_user.user_level());
|
||||
const QString userName = QString::fromStdString(m_user.name());
|
||||
const QString privLevel = QString::fromStdString(m_user.privlevel());
|
||||
const UserLevelFlags level(user.user_level());
|
||||
const QString userName = QString::fromStdString(user.name());
|
||||
const QString privLevel = QString::fromStdString(user.privlevel());
|
||||
|
||||
// Dark base
|
||||
p.fillRect(rect, QColor(14, 18, 26));
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Card art background ───────────────────────────────────────────────────
|
||||
if (!m_cardArt.isNull()) {
|
||||
if (!cardArt.isNull()) {
|
||||
const int w = rect.width();
|
||||
const int h = rect.height();
|
||||
const int mL = qRound(w * m_params.marginPctL);
|
||||
const int mR = qRound(w * m_params.marginPctR);
|
||||
const int mL = qRound(w * params.marginPctL);
|
||||
const int mR = qRound(w * params.marginPctR);
|
||||
const int dW = w - mL - mR;
|
||||
|
||||
const double base = qMax(double(dW) / m_cardArt.width(), double(h) / m_cardArt.height());
|
||||
const double scale = base * m_params.zoom;
|
||||
const int sW = qRound(m_cardArt.width() * scale);
|
||||
const int sH = qRound(m_cardArt.height() * scale);
|
||||
const 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 QPixmap scaled = m_cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
const QPixmap scaled = cardArt.scaled(sW, sH, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
const int srcX = (sW - dW) / 2;
|
||||
const int srcY = qBound(0, qRound((sH - h) * m_params.verticalOffset), qMax(0, sH - h));
|
||||
const int srcY = qBound(0, qRound((sH - h) * params.verticalOffset), qMax(0, sH - h));
|
||||
|
||||
QImage img = scaled.copy(srcX, srcY, dW, h).toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
{
|
||||
|
|
@ -155,12 +208,14 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.setOpacity(1.0);
|
||||
}
|
||||
|
||||
// Bottom gradient overlay so avatar and text are always legible
|
||||
// Bottom gradient overlay so avatar and text are always legible. The scrim
|
||||
// is the palette's Window color so it reads naturally in either scheme.
|
||||
{
|
||||
const QColor scrim = qApp->palette().color(QPalette::Window);
|
||||
QLinearGradient ov(0, 0, 0, rect.height());
|
||||
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));
|
||||
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));
|
||||
p.fillRect(rect, ov);
|
||||
}
|
||||
|
||||
|
|
@ -187,20 +242,20 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
p.save();
|
||||
p.setClipPath(clip);
|
||||
|
||||
if (!m_avatar.isNull()) {
|
||||
p.drawPixmap(ar, m_avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
|
||||
if (!avatar.isNull()) {
|
||||
p.drawPixmap(ar, avatar.scaled(ar.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(accent.darker(200));
|
||||
p.setBrush(UserListPainter::blend(accent, style.base, dark ? 0.45 : 0.72));
|
||||
p.drawEllipse(ar);
|
||||
const QPixmap pawn =
|
||||
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, m_user.pawn_colors(), false, privLevel);
|
||||
UserLevelPixmapGenerator::generatePixmap(AvatarPawnSize, level, user.pawn_colors(), false, privLevel);
|
||||
p.drawPixmap(ar.center().x() - AvatarPawnSize / 2, ar.center().y() - AvatarPawnSize / 2, pawn);
|
||||
}
|
||||
p.restore();
|
||||
|
||||
// Status ring
|
||||
p.setPen(QPen(m_online ? QColor(34, 197, 94) : QColor(70, 80, 95), 2.5));
|
||||
p.setPen(QPen(online ? QColor(34, 197, 94) : style.ringOffline, 2.5));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawEllipse(QRectF(ar).adjusted(-1.25, -1.25, 1.25, 1.25));
|
||||
|
||||
|
|
@ -212,7 +267,7 @@ void UserInfoHeaderWidget::paintEvent(QPaintEvent *)
|
|||
nf.setBold(true);
|
||||
nf.setPointSizeF(nf.pointSizeF() * 1.12);
|
||||
p.setFont(nf);
|
||||
p.setPen(m_online ? QColor(220, 228, 240) : QColor(90, 100, 115));
|
||||
p.setPen(online ? style.textOnline : style.textOffline);
|
||||
p.drawText(QRect(tx, ay, tw, AvatarSize / 2 + 4), Qt::AlignBottom | Qt::AlignLeft,
|
||||
QFontMetrics(nf).elidedText(userName, Qt::ElideRight, tw));
|
||||
|
||||
|
|
@ -243,143 +298,173 @@ 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(badge.color.darker(160));
|
||||
p.setBrush(UserListPainter::blend(badge.color, style.base, dark ? 0.55 : 0.78));
|
||||
p.drawRoundedRect(br, 3, 3);
|
||||
p.setPen(badge.color.lighter(150));
|
||||
p.setPen(dark ? UserListPainter::blend(badge.color, Qt::white, 0.5)
|
||||
: UserListPainter::blend(badge.color, Qt::black, 0.35));
|
||||
p.drawText(br, Qt::AlignCenter, badge.text);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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), m_ts(ts), m_client(client), m_avatarCache(avatarCache),
|
||||
m_cardArtCache(cardArtCache), m_cardArtParamsMap(cardArtParamsMap)
|
||||
: QFrame(parent, Qt::Tool | Qt::FramelessWindowHint), ts(_ts), client(_client), avatarCache(_avatarCache),
|
||||
cardArtCache(_cardArtCache), 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
|
||||
m_header = new UserInfoHeaderWidget(this);
|
||||
root->addWidget(m_header);
|
||||
header = new UserInfoHeaderWidget(this);
|
||||
root->addWidget(header);
|
||||
|
||||
// Action area — rebuilt per user
|
||||
m_actionArea = new QWidget(this);
|
||||
m_actionArea->setStyleSheet(QStringLiteral("background:#0e1218;"));
|
||||
root->addWidget(m_actionArea);
|
||||
actionArea = new QWidget(this);
|
||||
root->addWidget(actionArea);
|
||||
|
||||
// Thin separator
|
||||
auto *sep = new QFrame(this);
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setStyleSheet(QStringLiteral("color:#1a2434; margin: 0 8px;"));
|
||||
root->addWidget(sep);
|
||||
separator = new QFrame(this);
|
||||
separator->setFrameShape(QFrame::HLine);
|
||||
root->addWidget(separator);
|
||||
|
||||
// Games header row
|
||||
auto *gh = new QHBoxLayout;
|
||||
gh->setContentsMargins(10, 4, 8, 2);
|
||||
auto *gl = new QLabel(tr("Games"), this);
|
||||
gl->setStyleSheet(QStringLiteral("color:#6882a0; font-size:11px; font-weight:bold; background:transparent;"));
|
||||
gh->addWidget(gl);
|
||||
gamesLabel = new QLabel(tr("Games"), this);
|
||||
gh->addWidget(gamesLabel);
|
||||
gh->addStretch();
|
||||
m_refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||
m_refreshBtn->setFixedSize(20, 20);
|
||||
m_refreshBtn->setFlat(true);
|
||||
m_refreshBtn->setStyleSheet(
|
||||
QStringLiteral("QPushButton{color:#6882a0;border:none;font-size:14px;background:transparent;}"
|
||||
"QPushButton:hover{color:white;}"));
|
||||
connect(m_refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||
gh->addWidget(m_refreshBtn);
|
||||
refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||
refreshBtn->setFixedSize(20, 20);
|
||||
refreshBtn->setFlat(true);
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||
gh->addWidget(refreshBtn);
|
||||
root->addLayout(gh);
|
||||
|
||||
// Status label
|
||||
m_gamesStatus = new QLabel(this);
|
||||
m_gamesStatus->setAlignment(Qt::AlignCenter);
|
||||
m_gamesStatus->setStyleSheet(
|
||||
QStringLiteral("color:#3a4a5e; font-size:11px; padding:10px; background:transparent;"));
|
||||
root->addWidget(m_gamesStatus);
|
||||
gamesStatus = new QLabel(this);
|
||||
gamesStatus->setAlignment(Qt::AlignCenter);
|
||||
root->addWidget(gamesStatus);
|
||||
|
||||
// Games list
|
||||
m_gamesModel = new QStandardItemModel(this);
|
||||
m_gamesView = new QListView(this);
|
||||
m_gamesView->setModel(m_gamesModel);
|
||||
m_gamesView->setItemDelegate(new PopupGameDelegate(m_gamesView));
|
||||
m_gamesView->setFrameShape(QFrame::NoFrame);
|
||||
m_gamesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_gamesView->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
m_gamesView->setMaximumHeight(220);
|
||||
m_gamesView->setStyleSheet(QStringLiteral("QListView{background:#0e1218;border:none;}"
|
||||
"QListView::item:selected{background:#232e42;}"));
|
||||
m_gamesView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_gamesView, &QListView::customContextMenuRequested, this, &UserInfoPopup::onGamesContextMenu);
|
||||
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);
|
||||
|
||||
root->addWidget(m_gamesView);
|
||||
root->addWidget(gamesView);
|
||||
|
||||
// Close button — positioned absolutely in the top-right corner
|
||||
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
|
||||
m_closeBtn->setFixedSize(22, 22);
|
||||
m_closeBtn->setFlat(true);
|
||||
m_closeBtn->setStyleSheet(QStringLiteral("QPushButton{background:rgba(14,18,26,180);color:#607080;"
|
||||
"border:none;border-radius:11px;font-size:10px;}"
|
||||
"QPushButton:hover{color:white;background:rgba(200,50,50,200);}"));
|
||||
connect(m_closeBtn, &QPushButton::clicked, this, &UserInfoPopup::closeRequested);
|
||||
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();
|
||||
}
|
||||
|
||||
// ── Action button factory ─────────────────────────────────────────────────────
|
||||
|
||||
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p)
|
||||
static QPushButton *makeBtn(const QString &label, const QString &tip, QWidget *p, const PopupTheme &t)
|
||||
{
|
||||
auto *b = new QPushButton(label, p);
|
||||
b->setToolTip(tip);
|
||||
b->setFixedHeight(26);
|
||||
b->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
b->setStyleSheet(QStringLiteral("QPushButton{"
|
||||
" background:#192030;color:#b8c8de;border:1px solid #263040;"
|
||||
" background:%1;color:%2;border:1px solid %3;"
|
||||
" border-radius:4px;font-size:11px;padding:0 4px;"
|
||||
"}"
|
||||
"QPushButton:hover{background:#223050;color:white;}"
|
||||
"QPushButton:pressed{background:#162030;}"
|
||||
"QPushButton:disabled{color:#384858;border-color:#192030;}"));
|
||||
"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)));
|
||||
return b;
|
||||
}
|
||||
|
||||
void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||
{
|
||||
// Clear previous contents
|
||||
delete m_actionArea->layout();
|
||||
const auto old = m_actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
|
||||
delete actionArea->layout();
|
||||
const auto old = actionArea->findChildren<QPushButton *>(QString{}, Qt::FindDirectChildrenOnly);
|
||||
for (auto *w : old) {
|
||||
w->deleteLater();
|
||||
}
|
||||
|
||||
const QString name = QString::fromStdString(userInfo.name());
|
||||
const auto ownLevel = UserLevelFlags(m_ts->getUserInfo()->user_level());
|
||||
const bool isSelf = (name == QString::fromStdString(m_ts->getUserInfo()->name()));
|
||||
const auto ownLevel = UserLevelFlags(ts->getUserInfo()->user_level());
|
||||
const bool isSelf = (name == QString::fromStdString(ts->getUserInfo()->name()));
|
||||
const bool isMod = ownLevel.testFlag(ServerInfo_User::IsModerator);
|
||||
const bool isAdmin = ownLevel.testFlag(ServerInfo_User::IsAdmin);
|
||||
const auto their = UserLevelFlags(userInfo.user_level());
|
||||
const bool isReg = their.testFlag(ServerInfo_User::IsRegistered);
|
||||
|
||||
auto *grid = new QGridLayout(m_actionArea);
|
||||
auto *grid = new QGridLayout(actionArea);
|
||||
grid->setContentsMargins(8, 6, 8, 6);
|
||||
grid->setSpacing(4);
|
||||
|
||||
|
|
@ -394,16 +479,16 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
};
|
||||
|
||||
// ── Always visible ────────────────────────────────────────────────────────
|
||||
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), m_actionArea);
|
||||
auto *chat = makeBtn(tr("Chat"), tr("Open private chat"), actionArea, theme);
|
||||
chat->setEnabled(!isSelf && online);
|
||||
connect(chat, &QPushButton::clicked, this, [this, name] { emit chatRequested(name); });
|
||||
add(chat);
|
||||
|
||||
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), m_actionArea);
|
||||
auto *prof = makeBtn(tr("Profile"), tr("View user profile"), actionArea, theme);
|
||||
connect(prof, &QPushButton::clicked, this, [this, name] { emit detailsRequested(name); });
|
||||
add(prof);
|
||||
|
||||
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), m_actionArea);
|
||||
auto *games = makeBtn(tr("Games"), tr("Show this user's games"), actionArea, theme);
|
||||
games->setEnabled(!isSelf && online);
|
||||
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
|
||||
add(games);
|
||||
|
|
@ -411,20 +496,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"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Buddy"), tr("Remove from buddy list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit removeBuddyRequested(name); });
|
||||
add(b);
|
||||
} else {
|
||||
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Buddy"), tr("Add to buddy list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit addBuddyRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
if (isIgnored) {
|
||||
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Ignore"), tr("Remove from ignore list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit removeIgnoreRequested(name); });
|
||||
add(b);
|
||||
} else {
|
||||
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Ignore"), tr("Add to ignore list"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit addIgnoreRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
|
|
@ -437,10 +522,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"), 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);
|
||||
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);
|
||||
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); });
|
||||
|
|
@ -453,31 +538,31 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
|||
|
||||
// ── Admin actions ─────────────────────────────────────────────────────────
|
||||
if (!isSelf && isAdmin) {
|
||||
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), m_actionArea);
|
||||
auto *notes = makeBtn(tr("Notes"), tr("View admin notes"), actionArea, theme);
|
||||
connect(notes, &QPushButton::clicked, this, [this, name] { emit adminNotesRequested(name); });
|
||||
add(notes);
|
||||
|
||||
if (their.testFlag(ServerInfo_User::IsModerator)) {
|
||||
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Mod"), tr("Demote from moderator"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromModRequested(name); });
|
||||
add(b);
|
||||
} else if (isReg) {
|
||||
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Mod"), tr("Promote to moderator"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToModRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
if (their.testFlag(ServerInfo_User::IsJudge)) {
|
||||
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), m_actionArea);
|
||||
auto *b = makeBtn(tr("− Judge"), tr("Demote from judge"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit demoteFromJudgeRequested(name); });
|
||||
add(b);
|
||||
} else if (isReg) {
|
||||
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), m_actionArea);
|
||||
auto *b = makeBtn(tr("+ Judge"), tr("Promote to judge"), actionArea, theme);
|
||||
connect(b, &QPushButton::clicked, this, [this, name] { emit promoteToJudgeRequested(name); });
|
||||
add(b);
|
||||
}
|
||||
}
|
||||
|
||||
m_actionArea->adjustSize();
|
||||
actionArea->adjustSize();
|
||||
}
|
||||
|
||||
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||
|
|
@ -488,7 +573,7 @@ void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool on
|
|||
|
||||
void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
||||
{
|
||||
const QModelIndex idx = m_gamesView->indexAt(pos);
|
||||
const QModelIndex idx = gamesView->indexAt(pos);
|
||||
if (!idx.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -501,8 +586,9 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
|
||||
QMenu menu(this);
|
||||
menu.setStyleSheet(
|
||||
QStringLiteral("QMenu{background:#12182a;color:#c8d8ec;border:1px solid #1e2838;border-radius:4px;}"
|
||||
"QMenu::item:selected{background:#223050;}"));
|
||||
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)));
|
||||
|
||||
const bool canJoin = !game.started() && game.player_count() < game.max_players();
|
||||
QAction *join = menu.addAction(tr("Join game"));
|
||||
|
|
@ -513,7 +599,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
spec = menu.addAction(tr("Spectate"));
|
||||
}
|
||||
|
||||
const QAction *chosen = menu.exec(m_gamesView->viewport()->mapToGlobal(pos));
|
||||
const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos));
|
||||
if (!chosen) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -529,17 +615,17 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
|||
|
||||
void UserInfoPopup::refreshHeader()
|
||||
{
|
||||
if (m_currentUser.isEmpty()) {
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
const QPixmap avatar = avatarCache ? avatarCache->value(currentUser) : QPixmap{};
|
||||
const CardArtParams params = (cardArtParamsMap && cardArtParamsMap->contains(currentUser))
|
||||
? cardArtParamsMap->value(currentUser)
|
||||
: CardArtParams{};
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
void UserInfoPopup::showForUser(const QString &userName,
|
||||
|
|
@ -548,9 +634,9 @@ void UserInfoPopup::showForUser(const QString &userName,
|
|||
bool isBuddy,
|
||||
bool isIgnored)
|
||||
{
|
||||
m_currentUser = userName;
|
||||
m_currentUserInfo = userInfo;
|
||||
m_currentOnline = online;
|
||||
currentUser = userName;
|
||||
currentUserInfo = userInfo;
|
||||
currentOnline = online;
|
||||
|
||||
// Header
|
||||
refreshHeader();
|
||||
|
|
@ -559,14 +645,14 @@ void UserInfoPopup::showForUser(const QString &userName,
|
|||
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
||||
|
||||
// Games list reset
|
||||
m_gamesModel->clear();
|
||||
m_gamesView->hide();
|
||||
m_gamesStatus->setText(tr("Loading games…"));
|
||||
m_gamesStatus->show();
|
||||
gamesModel->clear();
|
||||
gamesView->hide();
|
||||
gamesStatus->setText(tr("Loading games…"));
|
||||
gamesStatus->show();
|
||||
|
||||
// Close button — top-right corner, above everything
|
||||
m_closeBtn->move(PopupWidth - m_closeBtn->width() - 6, 6);
|
||||
m_closeBtn->raise();
|
||||
closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
|
||||
closeBtn->raise();
|
||||
|
||||
adjustSize();
|
||||
fetchGames();
|
||||
|
|
@ -576,40 +662,40 @@ void UserInfoPopup::showForUser(const QString &userName,
|
|||
|
||||
void UserInfoPopup::fetchGames()
|
||||
{
|
||||
if (!m_client || m_currentUser.isEmpty()) {
|
||||
if (!client || currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_GetGamesOfUser cmd;
|
||||
cmd.set_user_name(m_currentUser.toStdString());
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
|
||||
const QString snapshot = m_currentUser;
|
||||
PendingCommand *pend = m_client->prepareSessionCommand(cmd);
|
||||
const QString snapshot = currentUser;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this,
|
||||
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
|
||||
m_client->sendCommand(pend);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
|
||||
{
|
||||
if (forUser != m_currentUser) {
|
||||
if (forUser != currentUser) {
|
||||
return; // stale response — different user showing now
|
||||
}
|
||||
|
||||
m_gamesModel->clear();
|
||||
gamesModel->clear();
|
||||
|
||||
if (r.response_code() != Response::RespOk) {
|
||||
m_gamesStatus->setText(tr("Could not load games."));
|
||||
m_gamesStatus->show();
|
||||
m_gamesView->hide();
|
||||
gamesStatus->setText(tr("Could not load games."));
|
||||
gamesStatus->show();
|
||||
gamesView->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
|
||||
if (resp.game_list_size() == 0) {
|
||||
m_gamesStatus->setText(tr("No active games."));
|
||||
m_gamesStatus->show();
|
||||
m_gamesView->hide();
|
||||
gamesStatus->setText(tr("No active games."));
|
||||
gamesStatus->show();
|
||||
gamesView->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -617,29 +703,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);
|
||||
m_gamesModel->appendRow(item);
|
||||
gamesModel->appendRow(item);
|
||||
}
|
||||
|
||||
m_gamesStatus->hide();
|
||||
m_gamesView->show();
|
||||
gamesStatus->hide();
|
||||
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 = m_gamesModel->rowCount();
|
||||
const int count = gamesModel->rowCount();
|
||||
const int visible = qMin(count, maxRows);
|
||||
m_gamesView->setFixedHeight(visible * rowH + 2);
|
||||
m_gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||
gamesView->setFixedHeight(visible * rowH + 2);
|
||||
gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void UserInfoPopup::refreshGames()
|
||||
{
|
||||
m_gamesModel->clear();
|
||||
m_gamesView->hide();
|
||||
m_gamesStatus->setText(tr("Loading games…"));
|
||||
m_gamesStatus->show();
|
||||
gamesModel->clear();
|
||||
gamesView->hide();
|
||||
gamesStatus->setText(tr("Loading games…"));
|
||||
gamesStatus->show();
|
||||
fetchGames();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,35 @@ 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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -51,21 +80,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 ¶ms);
|
||||
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 m_user;
|
||||
bool m_online = false;
|
||||
QPixmap m_avatar;
|
||||
QPixmap m_cardArt;
|
||||
CardArtParams m_params;
|
||||
ServerInfo_User user;
|
||||
bool online = false;
|
||||
QPixmap avatar;
|
||||
QPixmap cardArt;
|
||||
CardArtParams params;
|
||||
};
|
||||
|
||||
// ── Main popup ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -93,11 +122,11 @@ class UserInfoPopup : public QFrame
|
|||
static constexpr int PopupWidth = 316;
|
||||
|
||||
public:
|
||||
explicit UserInfoPopup(TabSupervisor *tabSupervisor,
|
||||
AbstractClient *client,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
explicit UserInfoPopup(TabSupervisor *_ts,
|
||||
AbstractClient *_client,
|
||||
const QMap<QString, QPixmap> *_avatarCache,
|
||||
const QMap<QString, QPixmap> *_cardArtCache,
|
||||
const QMap<QString, CardArtParams> *_cardArtParamsMap,
|
||||
QWidget *parent);
|
||||
|
||||
/**
|
||||
|
|
@ -108,9 +137,9 @@ public:
|
|||
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||
void fetchGames();
|
||||
|
||||
[[nodiscard]] QString currentUser() const
|
||||
[[nodiscard]] QString getCurrentUser() const
|
||||
{
|
||||
return m_currentUser;
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
/** Called when buddy/ignore status changes externally while popup is open. */
|
||||
|
|
@ -156,25 +185,30 @@ private slots:
|
|||
|
||||
private:
|
||||
void buildUi();
|
||||
void applyTheme();
|
||||
void rebuildActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||
|
||||
TabSupervisor *m_ts;
|
||||
AbstractClient *m_client;
|
||||
const QMap<QString, QPixmap> *m_avatarCache;
|
||||
const QMap<QString, QPixmap> *m_cardArtCache;
|
||||
const QMap<QString, CardArtParams> *m_cardArtParamsMap;
|
||||
TabSupervisor *ts;
|
||||
AbstractClient *client;
|
||||
const QMap<QString, QPixmap> *avatarCache;
|
||||
const QMap<QString, QPixmap> *cardArtCache;
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap;
|
||||
|
||||
QString m_currentUser;
|
||||
ServerInfo_User m_currentUserInfo;
|
||||
bool m_currentOnline = false;
|
||||
PopupTheme theme;
|
||||
|
||||
UserInfoHeaderWidget *m_header;
|
||||
QWidget *m_actionArea; ///< rebuilt per user
|
||||
QListView *m_gamesView;
|
||||
QStandardItemModel *m_gamesModel;
|
||||
QLabel *m_gamesStatus;
|
||||
QPushButton *m_closeBtn;
|
||||
QPushButton *m_refreshBtn;
|
||||
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;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_INFO_POPUP_H
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../interface/pixel_map_generator.h"
|
||||
|
||||
#include <QAbstractScrollArea>
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QScrollBar>
|
||||
|
|
@ -19,6 +20,29 @@ 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;
|
||||
|
|
@ -59,18 +83,41 @@ int UserListPainter::getCardRight(const QStyleOptionViewItem &option, const QRec
|
|||
void UserListPainter::drawBackground(QPainter *painter,
|
||||
const QRectF &cardRect,
|
||||
const QColor &accentColor,
|
||||
bool selected)
|
||||
bool selected,
|
||||
const Style &style,
|
||||
bool hasRole)
|
||||
{
|
||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||
bg.setColorAt(0, selected ? accentColor.darker(130) : accentColor.darker(320));
|
||||
bg.setColorAt(1, selected ? QColor(40, 48, 60) : QColor(18, 22, 30));
|
||||
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));
|
||||
}
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(bg);
|
||||
painter->drawRoundedRect(cardRect, 6, 6);
|
||||
|
||||
painter->setBrush(accentColor);
|
||||
painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
if (style.dark || hasRole || selected) {
|
||||
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)
|
||||
|
|
@ -163,7 +210,8 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
const UserLevelFlags &userLevel,
|
||||
const ServerInfo_User &userInfo,
|
||||
const QString &privLevel,
|
||||
const QMap<QString, QPixmap> *avatarCache)
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const Style &style)
|
||||
{
|
||||
QPainterPath clipPath;
|
||||
clipPath.addEllipse(avatarRect);
|
||||
|
|
@ -183,7 +231,7 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
}
|
||||
|
||||
if (!drewAvatar) {
|
||||
painter->setBrush(accentColor.darker(200));
|
||||
painter->setBrush(blend(accentColor, style.base, style.dark ? 0.45 : 0.72));
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->drawEllipse(avatarRect);
|
||||
|
||||
|
|
@ -196,9 +244,9 @@ void UserListPainter::drawAvatar(QPainter *painter,
|
|||
painter->restore();
|
||||
}
|
||||
|
||||
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online)
|
||||
void UserListPainter::drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style)
|
||||
{
|
||||
const QColor statusColor = online ? QColor(34, 197, 94) : QColor(70, 80, 95);
|
||||
const QColor statusColor = online ? QColor(34, 197, 94) : style.ringOffline;
|
||||
|
||||
painter->setPen(QPen(statusColor, 2));
|
||||
painter->setBrush(Qt::NoBrush);
|
||||
|
|
@ -212,7 +260,7 @@ void UserListPainter::drawUserName(QPainter *painter,
|
|||
int textX,
|
||||
const QString &userName,
|
||||
bool online,
|
||||
bool selected)
|
||||
const Style &style)
|
||||
{
|
||||
QFont nameFont = option.font;
|
||||
nameFont.setBold(true);
|
||||
|
|
@ -221,10 +269,12 @@ 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);
|
||||
|
||||
painter->setPen(QColor(0, 0, 0, 200));
|
||||
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
if (style.dropShadow) {
|
||||
painter->setPen(QColor(0, 0, 0, 200));
|
||||
painter->drawText(nameRect.translated(1, 1), Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
}
|
||||
|
||||
painter->setPen(online ? (selected ? Qt::white : QColor(226, 232, 240)) : QColor(90, 100, 115));
|
||||
painter->setPen(online ? style.textOnline : style.textOffline);
|
||||
painter->drawText(nameRect, Qt::AlignVCenter | Qt::AlignLeft, elidedName);
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +312,8 @@ void UserListPainter::drawBadges(QPainter *painter,
|
|||
const QRect &rect,
|
||||
int cardRight,
|
||||
const QList<Badge> &badges,
|
||||
bool online)
|
||||
bool online,
|
||||
const Style &style)
|
||||
{
|
||||
if (badges.isEmpty()) {
|
||||
return;
|
||||
|
|
@ -284,15 +335,17 @@ void UserListPainter::drawBadges(QPainter *painter,
|
|||
int bx = cardRight - 6 - totalBadgeW;
|
||||
|
||||
for (const Badge &b : badges) {
|
||||
const QColor col = online ? b.color : b.color.darker(180);
|
||||
const QColor col = online ? b.color : blend(b.color, style.base, 0.55);
|
||||
const QColor surface = blend(col, style.base, style.dark ? 0.55 : 0.78);
|
||||
const QColor text = style.dark ? blend(col, Qt::white, 0.5) : blend(col, Qt::black, 0.35);
|
||||
const int bw = fm.horizontalAdvance(b.text) + 8;
|
||||
const QRect br(bx, rect.top() + 44, bw, 13);
|
||||
|
||||
painter->setPen(Qt::NoPen);
|
||||
painter->setBrush(col.darker(online ? 160 : 220));
|
||||
painter->setBrush(surface);
|
||||
painter->drawRoundedRect(br, 3, 3);
|
||||
|
||||
painter->setPen(col.lighter(online ? 160 : 100));
|
||||
painter->setPen(text);
|
||||
painter->drawText(br, Qt::AlignCenter, b.text);
|
||||
|
||||
bx += bw + 4;
|
||||
|
|
@ -305,11 +358,19 @@ void UserListPainter::paint(QPainter *painter,
|
|||
const ServerInfo_User &userInfo,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap)
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
bool dark)
|
||||
{
|
||||
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;
|
||||
|
|
@ -317,6 +378,9 @@ 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);
|
||||
|
||||
|
|
@ -324,19 +388,19 @@ void UserListPainter::paint(QPainter *painter,
|
|||
? cardArtParamsMap->value(userName)
|
||||
: CardArtParams{};
|
||||
|
||||
drawBackground(painter, cardRect, accentColor, selected);
|
||||
drawBackground(painter, cardRect, accentColor, selected, style, hasRole);
|
||||
drawCardArt(painter, rect, cardRight, userName, cardArtCache, params);
|
||||
|
||||
const QRect avatarRect = getAvatarRect(rect);
|
||||
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache);
|
||||
drawStatusRing(painter, avatarRect, online);
|
||||
drawAvatar(painter, avatarRect, userName, accentColor, userLevel, userInfo, privLevel, avatarCache, style);
|
||||
drawStatusRing(painter, avatarRect, online, style);
|
||||
|
||||
const int textX = avatarRect.right() + TextSpacing;
|
||||
drawUserName(painter, option, rect, cardRight, textX, userName, online, selected);
|
||||
drawUserName(painter, option, rect, cardRight, textX, userName, online, style);
|
||||
drawCountryFlag(painter, rect, textX, userInfo);
|
||||
|
||||
const QList<Badge> badges = buildBadges(userLevel, privLevel);
|
||||
drawBadges(painter, option, rect, cardRight, badges, online);
|
||||
drawBadges(painter, option, rect, cardRight, badges, online, style);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QPalette>
|
||||
#include <QPixmap>
|
||||
#include <QRect>
|
||||
#include <QSize>
|
||||
|
|
@ -28,13 +29,36 @@ 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);
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap,
|
||||
bool dark);
|
||||
|
||||
static QSize sizeHint();
|
||||
|
||||
|
|
@ -55,7 +79,12 @@ 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);
|
||||
static void drawBackground(QPainter *painter,
|
||||
const QRectF &cardRect,
|
||||
const QColor &accentColor,
|
||||
bool selected,
|
||||
const Style &style,
|
||||
bool hasRole);
|
||||
static QRect getAvatarRect(const QRect &rect);
|
||||
static void drawAvatar(QPainter *painter,
|
||||
const QRect &avatarRect,
|
||||
|
|
@ -64,8 +93,9 @@ private:
|
|||
const UserLevelFlags &userLevel,
|
||||
const ServerInfo_User &userInfo,
|
||||
const QString &privLevel,
|
||||
const QMap<QString, QPixmap> *avatarCache);
|
||||
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online);
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const Style &style);
|
||||
static void drawStatusRing(QPainter *painter, const QRect &avatarRect, bool online, const Style &style);
|
||||
static void drawUserName(QPainter *painter,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QRect &rect,
|
||||
|
|
@ -73,7 +103,7 @@ private:
|
|||
int textX,
|
||||
const QString &userName,
|
||||
bool online,
|
||||
bool selected);
|
||||
const Style &style);
|
||||
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,
|
||||
|
|
@ -81,7 +111,8 @@ private:
|
|||
const QRect &rect,
|
||||
int cardRight,
|
||||
const QList<Badge> &badges,
|
||||
bool online);
|
||||
bool online,
|
||||
const Style &style);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_USER_LIST_PAINTER_H
|
||||
#endif // COCKATRICE_USER_LIST_PAINTER_H
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
#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;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,7 @@
|
|||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
#include <QQueue>
|
||||
#include <QSet>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTextEdit>
|
||||
#include <QTreeWidgetItem>
|
||||
|
|
@ -103,12 +104,13 @@ 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(QObject *const parent,
|
||||
explicit UserListItemDelegate(QTreeWidget *tree,
|
||||
const QMap<QString, QPixmap> *avatarCache,
|
||||
const QMap<QString, QPixmap> *cardArtCache,
|
||||
const QMap<QString, CardArtParams> *cardArtParamsMap);
|
||||
|
|
@ -147,6 +149,12 @@ public:
|
|||
BuddyList,
|
||||
IgnoreList
|
||||
};
|
||||
enum class Section
|
||||
{
|
||||
Buddy,
|
||||
Online,
|
||||
Ignore
|
||||
};
|
||||
|
||||
private:
|
||||
UserListManager *manager = nullptr;
|
||||
|
|
@ -154,30 +162,69 @@ private:
|
|||
UserCardArtProvider *cardArtProvider = nullptr;
|
||||
QMap<QString, CardArtParams> cardArtParamsMap;
|
||||
// ── Hover popup ───────────────────────────────────────────────────────────
|
||||
UserInfoPopup *m_userInfoPopup = nullptr;
|
||||
QTimer *m_showPopupTimer = nullptr;
|
||||
QTimer *m_hidePopupTimer = nullptr;
|
||||
QString m_hoveredUser;
|
||||
bool m_popupPinned = false;
|
||||
bool m_bulkLoading = false;
|
||||
UserInfoPopup *userInfoPopup = nullptr;
|
||||
QTimer *showPopupTimer = nullptr;
|
||||
QTimer *hidePopupTimer = nullptr;
|
||||
QString hoveredUser;
|
||||
bool popupPinned = false;
|
||||
bool bulkLoading = false;
|
||||
|
||||
void showPopupForUser(const QString &userName);
|
||||
/**
|
||||
* 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 hidePopup(bool immediate = false);
|
||||
void positionPopup(const QString &userName);
|
||||
void positionPopup(UserListTWI *item);
|
||||
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;
|
||||
QTreeWidget *userTree = nullptr;
|
||||
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);
|
||||
|
|
@ -189,12 +236,14 @@ 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();
|
||||
|
|
@ -205,6 +254,14 @@ 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;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../main.h"
|
||||
#include "../server/user/user_info_connection.h"
|
||||
#include "update/client/release_channel.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
#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>
|
||||
|
||||
|
|
@ -121,8 +123,61 @@ 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);
|
||||
|
|
@ -357,6 +412,17 @@ 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"));
|
||||
|
|
@ -393,6 +459,20 @@ 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();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ private slots:
|
|||
void tokenDatabasePathButtonClicked();
|
||||
void resetAllPathsClicked();
|
||||
void languageBoxChanged(int index);
|
||||
void updateStartupServerControlsVisibility();
|
||||
|
||||
private:
|
||||
QStringList findQmFiles();
|
||||
|
|
@ -71,6 +72,12 @@ 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
|
||||
|
|
|
|||
|
|
@ -183,6 +183,13 @@ 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);
|
||||
|
|
@ -242,10 +249,12 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
|
|||
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
|
||||
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1);
|
||||
deckEditorGrid->addWidget(labelWidget, 6, 0);
|
||||
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 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);
|
||||
|
||||
deckEditorGroupBox = new QGroupBox;
|
||||
deckEditorGroupBox->setLayout(deckEditorGrid);
|
||||
|
|
@ -368,6 +377,12 @@ 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"));
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ private:
|
|||
QCheckBox visualDeckStorageSelectionAnimationCheckBox;
|
||||
QLabel defaultDeckEditorTypeLabel;
|
||||
QComboBox defaultDeckEditorTypeSelector;
|
||||
QLabel vdeStartupTabLabel;
|
||||
QComboBox vdeStartupTabSelector;
|
||||
QLabel commanderSpellbookIntegrationEnabledLabel;
|
||||
QComboBox commanderSpellbookIntegrationEnabledSelector;
|
||||
QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#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"
|
||||
|
|
@ -60,23 +61,10 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
|||
tempMap.insert(info.room_id(), gameTypes);
|
||||
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
|
||||
|
||||
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"));
|
||||
userListPanel = new UserListPanelWidget(tabSupervisor, client, this);
|
||||
userListPanel->bind(tabSupervisor->getUserListManager());
|
||||
userList = userListPanel->getUserList();
|
||||
connect(userListPanel, &UserListPanelWidget::openMessageDialog, this, &TabRoom::openMessageDialog);
|
||||
|
||||
chatView = new ChatView(tabSupervisor, nullptr, true, this);
|
||||
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
|
||||
|
|
@ -126,7 +114,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
|
|||
|
||||
auto *hbox = new QHBoxLayout;
|
||||
hbox->addWidget(splitter, 3);
|
||||
hbox->addWidget(tabs, 1);
|
||||
hbox->addWidget(userListPanel, 1);
|
||||
|
||||
aLeaveRoom = new QAction(this);
|
||||
connect(aLeaveRoom, &QAction::triggered, this, &TabRoom::closeRequest);
|
||||
|
|
@ -181,7 +169,7 @@ void TabRoom::retranslateUi()
|
|||
{
|
||||
gameSelector->retranslateUi();
|
||||
chatView->retranslateUi();
|
||||
userList->retranslateUi();
|
||||
userListPanel->retranslateUi();
|
||||
sayLabel->setText(tr("&Say:"));
|
||||
chatGroupBox->setTitle(tr("Chat"));
|
||||
roomMenu->setTitle(tr("&Room"));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class Message;
|
|||
} // namespace google
|
||||
class AbstractClient;
|
||||
class UserListWidget;
|
||||
class UserListPanelWidget;
|
||||
class QLabel;
|
||||
class ChatView;
|
||||
class QPushButton;
|
||||
|
|
@ -57,9 +58,8 @@ private:
|
|||
QMap<int, QString> gameTypes;
|
||||
|
||||
GameSelector *gameSelector;
|
||||
UserListWidget *friendsList;
|
||||
UserListPanelWidget *userListPanel;
|
||||
UserListWidget *userList;
|
||||
UserListWidget *ignoreList;
|
||||
const UserListProxy *userListProxy;
|
||||
ChatView *chatView;
|
||||
QLabel *sayLabel;
|
||||
|
|
@ -114,6 +114,10 @@ public:
|
|||
{
|
||||
return roomId;
|
||||
}
|
||||
[[nodiscard]] QString getRoomName() const
|
||||
{
|
||||
return roomName;
|
||||
}
|
||||
[[nodiscard]] const QMap<int, QString> &getGameTypes() const
|
||||
{
|
||||
return gameTypes;
|
||||
|
|
|
|||
|
|
@ -335,7 +335,12 @@ static void checkAndTrigger(QAction *checkableAction, bool checked)
|
|||
}
|
||||
|
||||
/**
|
||||
* Opens the always-available tabs, depending on settings.
|
||||
* 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.
|
||||
*/
|
||||
void TabSupervisor::initStartupTabs()
|
||||
{
|
||||
|
|
@ -351,6 +356,42 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ public slots:
|
|||
void actTabVisualDeckStorage(bool checked);
|
||||
void actTabReplays(bool checked);
|
||||
void openTabServer();
|
||||
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
|
||||
private slots:
|
||||
void refreshShortcuts();
|
||||
|
||||
|
|
@ -209,7 +210,6 @@ 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);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
#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>
|
||||
|
||||
/**
|
||||
|
|
@ -98,6 +99,33 @@ 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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -164,6 +164,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -49,6 +49,17 @@ 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.
|
||||
|
|
|
|||
|
|
@ -32,8 +32,14 @@
|
|||
#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"
|
||||
|
|
@ -77,6 +83,7 @@
|
|||
#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"
|
||||
|
|
@ -540,6 +547,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
|
||||
// run startup check async
|
||||
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
|
||||
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
|
||||
}
|
||||
|
||||
void MainWindow::startupConfigCheck()
|
||||
|
|
@ -648,6 +656,82 @@ 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) {
|
||||
|
|
@ -750,7 +834,8 @@ 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()) {
|
||||
!SettingsCache::instance().debug().getLocalGameOnStartup() &&
|
||||
!startupDestinationConnectsToServer()) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
|
||||
DlgConnect dlg(this);
|
||||
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class ServerInfo_User;
|
|||
class TabSupervisor;
|
||||
class WndSets;
|
||||
class DlgTipOfTheDay;
|
||||
struct ContextConnectToServer;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
|
|
@ -105,6 +106,11 @@ 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;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
class IInterfaceSettingsProvider
|
||||
{
|
||||
|
|
@ -41,6 +42,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
#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;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ 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");
|
||||
|
|
@ -47,6 +52,12 @@ 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(),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ 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
|
||||
|
|
@ -23,6 +30,7 @@ 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;
|
||||
|
||||
|
|
@ -30,6 +38,7 @@ 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);
|
||||
|
||||
|
|
@ -38,6 +47,7 @@ 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);
|
||||
|
|
|
|||
|
|
@ -170,6 +170,12 @@ 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");
|
||||
|
|
@ -348,3 +354,8 @@ void InterfaceSettings::setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled
|
|||
setValue(_battlefieldFlashEnabled, "battlefieldFlashEnabled");
|
||||
emit battlefieldFlashEnabledChanged(_battlefieldFlashEnabled);
|
||||
}
|
||||
|
||||
void InterfaceSettings::setUserListExpandedSections(const QStringList §ions)
|
||||
{
|
||||
setValue(sections, "userListExpandedSections");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ 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);
|
||||
|
|
@ -78,6 +79,7 @@ public:
|
|||
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
|
||||
void setLifeCounterAnimationsEnabled(bool _lifeCounterAnimationsEnabled);
|
||||
void setBattlefieldFlashEnabled(bool _battlefieldFlashEnabled);
|
||||
void setUserListExpandedSections(const QStringList §ions);
|
||||
|
||||
signals:
|
||||
void useTearOffMenusChanged(bool state);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,26 @@ 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();
|
||||
|
|
@ -40,6 +60,42 @@ 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");
|
||||
|
|
|
|||
|
|
@ -5,12 +5,35 @@
|
|||
|
||||
#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;
|
||||
|
|
@ -19,6 +42,10 @@ 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);
|
||||
|
|
@ -27,6 +54,12 @@ 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,38 @@ 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);
|
||||
|
|
@ -400,6 +432,23 @@ 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue