mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Fix user card games for offline users and stale requests (#7738)
This commit is contained in:
parent
7768416091
commit
94f7de42f2
8 changed files with 550 additions and 76 deletions
|
|
@ -449,6 +449,8 @@ set(cockatrice_SOURCES
|
||||||
src/interface/intents/intent_open_shared_deck.h
|
src/interface/intents/intent_open_shared_deck.h
|
||||||
src/interface/intents/url_parser.cpp
|
src/interface/intents/url_parser.cpp
|
||||||
src/interface/intents/url_parser.h
|
src/interface/intents/url_parser.h
|
||||||
|
src/interface/widgets/server/user/user_games_model.cpp
|
||||||
|
src/interface/widgets/server/user/user_games_model.h
|
||||||
src/interface/widgets/server/user/user_info_popup.cpp
|
src/interface/widgets/server/user/user_info_popup.cpp
|
||||||
src/interface/widgets/server/user/user_info_popup.h
|
src/interface/widgets/server/user/user_info_popup.h
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
#include "user_games_model.h"
|
||||||
|
|
||||||
|
#include <QVariant>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/session_commands.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
UserGamesModel::UserGamesModel(AbstractClient *_client, int requestTimeoutMs, QObject *parent)
|
||||||
|
: QAbstractListModel(parent), client(_client)
|
||||||
|
{
|
||||||
|
timeout.setSingleShot(true);
|
||||||
|
timeout.setInterval(requestTimeoutMs);
|
||||||
|
connect(&timeout, &QTimer::timeout, this, [this] {
|
||||||
|
invalidateRequest();
|
||||||
|
setState(State::Error);
|
||||||
|
});
|
||||||
|
connected = client->getStatus() == StatusLoggedIn;
|
||||||
|
connect(client, &AbstractClient::statusChanged, this, [this](ClientStatus status) {
|
||||||
|
// AbstractClient::setStatus emits this signal while holding the (non-recursive)
|
||||||
|
// status mutex, which getStatus() would try to lock again. Track the emitted
|
||||||
|
// value here, and read this cache from canRefresh() for the same reason.
|
||||||
|
connected = status == StatusLoggedIn;
|
||||||
|
if (!connected) {
|
||||||
|
invalidateRequest();
|
||||||
|
replaceGames({});
|
||||||
|
setState(online ? State::Disconnected : State::Offline);
|
||||||
|
} else if (online) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(client, &QObject::destroyed, this, [this] {
|
||||||
|
client = nullptr;
|
||||||
|
connected = false;
|
||||||
|
invalidateRequest();
|
||||||
|
replaceGames({});
|
||||||
|
setState(online ? State::Disconnected : State::Offline);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int UserGamesModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
return parent.isValid() ? 0 : games.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant UserGamesModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.model() != this || index.column() != 0 || index.row() < 0 ||
|
||||||
|
index.row() >= games.size() || role != GameRole) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QVariant::fromValue(games.at(index.row()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::setUser(const QString &name, bool isOnline)
|
||||||
|
{
|
||||||
|
userName = name;
|
||||||
|
online = isOnline;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserGamesModel::State UserGamesModel::getState() const
|
||||||
|
{
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UserGamesModel::canRefresh() const
|
||||||
|
{
|
||||||
|
return online && !userName.isEmpty() && client && connected && state != State::Loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::invalidateRequest()
|
||||||
|
{
|
||||||
|
++requestGeneration;
|
||||||
|
// The client owns pending commands. Detach our interest without deleting
|
||||||
|
// an object that may already have moved to the client's thread.
|
||||||
|
disconnect(responseConnection);
|
||||||
|
responseConnection = {};
|
||||||
|
timeout.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::replaceGames(QList<ServerInfo_Game> replacement)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
games = std::move(replacement);
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::setState(State newState)
|
||||||
|
{
|
||||||
|
state = newState;
|
||||||
|
emit stateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::refresh()
|
||||||
|
{
|
||||||
|
invalidateRequest();
|
||||||
|
const quint64 request = requestGeneration;
|
||||||
|
replaceGames({});
|
||||||
|
if (!online || userName.isEmpty()) {
|
||||||
|
setState(State::Offline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!client || !connected) {
|
||||||
|
setState(State::Disconnected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(State::Loading);
|
||||||
|
// State listeners may select another user or disconnect the client.
|
||||||
|
if (request != requestGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command_GetGamesOfUser command;
|
||||||
|
command.set_user_name(userName.toStdString());
|
||||||
|
PendingCommand *pending = client->prepareSessionCommand(command);
|
||||||
|
responseConnection = connect(pending, &PendingCommand::finished, this,
|
||||||
|
[this, request](const Response &response) { finishRequest(response, request); });
|
||||||
|
timeout.start();
|
||||||
|
client->sendCommand(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UserGamesModel::finishRequest(const Response &response, quint64 request)
|
||||||
|
{
|
||||||
|
if (request != requestGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalidateRequest();
|
||||||
|
const quint64 completion = requestGeneration;
|
||||||
|
if (response.response_code() != Response::RespOk || !response.HasExtension(Response_GetGamesOfUser::ext)) {
|
||||||
|
setState(State::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &result = response.GetExtension(Response_GetGamesOfUser::ext);
|
||||||
|
QList<ServerInfo_Game> replacement;
|
||||||
|
replacement.reserve(result.game_list_size());
|
||||||
|
for (const auto &game : result.game_list()) {
|
||||||
|
replacement.append(game);
|
||||||
|
}
|
||||||
|
replaceGames(std::move(replacement));
|
||||||
|
if (completion != requestGeneration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(games.isEmpty() ? State::Empty : State::Ready);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#ifndef COCKATRICE_USER_GAMES_MODEL_H
|
||||||
|
#define COCKATRICE_USER_GAMES_MODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
|
||||||
|
|
||||||
|
class AbstractClient;
|
||||||
|
class Response;
|
||||||
|
|
||||||
|
/** Current user's games and the lifetime of the request that supplies them. */
|
||||||
|
class UserGamesModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class State
|
||||||
|
{
|
||||||
|
Offline,
|
||||||
|
Disconnected,
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Empty,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
static constexpr int DefaultRequestTimeoutMs = 15000;
|
||||||
|
static constexpr int GameRole = Qt::UserRole + 10;
|
||||||
|
|
||||||
|
explicit UserGamesModel(AbstractClient *client,
|
||||||
|
int requestTimeoutMs = DefaultRequestTimeoutMs,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
int rowCount(const QModelIndex &parent = {}) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
void setUser(const QString &name, bool isOnline);
|
||||||
|
void refresh();
|
||||||
|
State getState() const;
|
||||||
|
bool canRefresh() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void stateChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void invalidateRequest();
|
||||||
|
void replaceGames(QList<ServerInfo_Game> replacement);
|
||||||
|
void finishRequest(const Response &response, quint64 request);
|
||||||
|
void setState(State newState);
|
||||||
|
|
||||||
|
QPointer<AbstractClient> client;
|
||||||
|
QString userName;
|
||||||
|
bool online = false;
|
||||||
|
bool connected = false;
|
||||||
|
State state = State::Offline;
|
||||||
|
QList<ServerInfo_Game> games;
|
||||||
|
quint64 requestGeneration = 0;
|
||||||
|
QMetaObject::Connection responseConnection;
|
||||||
|
QTimer timeout;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_USER_GAMES_MODEL_H
|
||||||
|
|
@ -6,10 +6,9 @@
|
||||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||||
#include "libcockatrice/card/database/card_database_querier.h"
|
#include "libcockatrice/card/database/card_database_querier.h"
|
||||||
#include "libcockatrice/card/printing/exact_card.h"
|
#include "libcockatrice/card/printing/exact_card.h"
|
||||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
|
||||||
#include "libcockatrice/protocol/pb/serverinfo_game.pb.h"
|
#include "libcockatrice/protocol/pb/serverinfo_game.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
|
||||||
#include "libcockatrice/protocol/pb/session_commands.pb.h"
|
#include "user_games_model.h"
|
||||||
#include "user_level.h"
|
#include "user_level.h"
|
||||||
#include "user_list_painter.h"
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
|
|
@ -34,6 +33,7 @@
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
|
#include <QPersistentModelIndex>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
|
|
@ -41,8 +41,6 @@
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
#include <QStandardItem>
|
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QStyleOptionViewItem>
|
#include <QStyleOptionViewItem>
|
||||||
|
|
@ -52,8 +50,6 @@
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
|
||||||
#include <libcockatrice/utility/card_ref.h>
|
#include <libcockatrice/utility/card_ref.h>
|
||||||
#include <libcockatrice/utility/qt_utils.h>
|
#include <libcockatrice/utility/qt_utils.h>
|
||||||
#include <qnumeric.h>
|
#include <qnumeric.h>
|
||||||
|
|
@ -427,6 +423,7 @@ void UserInfoPopup::buildUi()
|
||||||
refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
refreshBtn = new QPushButton(QStringLiteral("↻"), this);
|
||||||
refreshBtn->setFixedSize(20, 20);
|
refreshBtn->setFixedSize(20, 20);
|
||||||
refreshBtn->setFlat(true);
|
refreshBtn->setFlat(true);
|
||||||
|
refreshBtn->setToolTip(tr("Refresh games"));
|
||||||
connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
connect(refreshBtn, &QPushButton::clicked, this, &UserInfoPopup::refreshGames);
|
||||||
gh->addWidget(refreshBtn);
|
gh->addWidget(refreshBtn);
|
||||||
root->addLayout(gh);
|
root->addLayout(gh);
|
||||||
|
|
@ -437,7 +434,8 @@ void UserInfoPopup::buildUi()
|
||||||
root->addWidget(gamesStatus);
|
root->addWidget(gamesStatus);
|
||||||
|
|
||||||
// Games list
|
// Games list
|
||||||
gamesModel = new QStandardItemModel(this);
|
gamesModel = new UserGamesModel(client, UserGamesModel::DefaultRequestTimeoutMs, this);
|
||||||
|
connect(gamesModel, &UserGamesModel::stateChanged, this, &UserInfoPopup::updateGamesDisplay);
|
||||||
gamesView = new QListView(this);
|
gamesView = new QListView(this);
|
||||||
gamesView->setModel(gamesModel);
|
gamesView->setModel(gamesModel);
|
||||||
gamesView->setItemDelegate(new PopupGameDelegate(gamesView));
|
gamesView->setItemDelegate(new PopupGameDelegate(gamesView));
|
||||||
|
|
@ -645,13 +643,21 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
|
||||||
|
|
||||||
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
void UserInfoPopup::updateActionButtons(const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored)
|
||||||
{
|
{
|
||||||
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
currentUserInfo = userInfo;
|
||||||
adjustSize();
|
currentBuddy = isBuddy;
|
||||||
|
currentIgnored = isIgnored;
|
||||||
|
if (currentOnline != online) {
|
||||||
|
setOnline(online);
|
||||||
|
} else {
|
||||||
|
refreshHeader();
|
||||||
|
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
||||||
|
adjustSize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
||||||
{
|
{
|
||||||
const QModelIndex idx = gamesView->indexAt(pos);
|
const QPersistentModelIndex idx = gamesView->indexAt(pos);
|
||||||
if (!idx.isValid()) {
|
if (!idx.isValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -679,7 +685,7 @@ void UserInfoPopup::onGamesContextMenu(const QPoint &pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos));
|
const QAction *chosen = menu.exec(gamesView->viewport()->mapToGlobal(pos));
|
||||||
if (!chosen) {
|
if (!chosen || !idx.isValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -716,6 +722,8 @@ void UserInfoPopup::showForUser(const QString &userName,
|
||||||
currentUser = userName;
|
currentUser = userName;
|
||||||
currentUserInfo = userInfo;
|
currentUserInfo = userInfo;
|
||||||
currentOnline = online;
|
currentOnline = online;
|
||||||
|
currentBuddy = isBuddy;
|
||||||
|
currentIgnored = isIgnored;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
refreshHeader();
|
refreshHeader();
|
||||||
|
|
@ -723,88 +731,71 @@ void UserInfoPopup::showForUser(const QString &userName,
|
||||||
// Actions
|
// Actions
|
||||||
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
rebuildActionButtons(userInfo, online, isBuddy, isIgnored);
|
||||||
|
|
||||||
// Games list reset
|
|
||||||
gamesModel->clear();
|
|
||||||
gamesView->hide();
|
|
||||||
gamesStatus->setText(tr("Loading games…"));
|
|
||||||
gamesStatus->show();
|
|
||||||
|
|
||||||
// Close button, top right corner, above everything
|
// Close button, top right corner, above everything
|
||||||
closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
|
closeBtn->move(PopupWidth - closeBtn->width() - 6, 6);
|
||||||
closeBtn->raise();
|
closeBtn->raise();
|
||||||
|
|
||||||
|
gamesModel->setUser(currentUser, currentOnline);
|
||||||
adjustSize();
|
adjustSize();
|
||||||
fetchGames();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Games fetch ───────────────────────────────────────────────────────────────
|
// ── Games fetch ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void UserInfoPopup::fetchGames()
|
void UserInfoPopup::fetchGames()
|
||||||
{
|
{
|
||||||
if (!client || currentUser.isEmpty()) {
|
gamesModel->refresh();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Command_GetGamesOfUser cmd;
|
|
||||||
cmd.set_user_name(currentUser.toStdString());
|
|
||||||
|
|
||||||
const QString snapshot = currentUser;
|
|
||||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
|
||||||
connect(pend, &PendingCommand::finished, this,
|
|
||||||
[this, snapshot](const Response &r) { onGamesReceived(r, snapshot); });
|
|
||||||
client->sendCommand(pend);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserInfoPopup::onGamesReceived(const Response &r, const QString &forUser)
|
void UserInfoPopup::setOnline(bool online)
|
||||||
{
|
{
|
||||||
if (forUser != currentUser) {
|
if (currentOnline == online) {
|
||||||
return; // stale response, different user showing now
|
|
||||||
}
|
|
||||||
|
|
||||||
gamesModel->clear();
|
|
||||||
|
|
||||||
if (r.response_code() != Response::RespOk) {
|
|
||||||
gamesStatus->setText(tr("Could not load games."));
|
|
||||||
gamesStatus->show();
|
|
||||||
gamesView->hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
currentOnline = online;
|
||||||
|
refreshHeader();
|
||||||
|
rebuildActionButtons(currentUserInfo, online, currentBuddy, currentIgnored);
|
||||||
|
gamesModel->setUser(currentUser, online);
|
||||||
|
}
|
||||||
|
|
||||||
const auto &resp = r.GetExtension(Response_GetGamesOfUser::ext);
|
void UserInfoPopup::updateGamesDisplay()
|
||||||
if (resp.game_list_size() == 0) {
|
{
|
||||||
gamesStatus->setText(tr("No active games."));
|
refreshBtn->setEnabled(gamesModel->canRefresh());
|
||||||
gamesStatus->show();
|
const auto state = gamesModel->getState();
|
||||||
gamesView->hide();
|
const bool ready = state == UserGamesModel::State::Ready;
|
||||||
return;
|
gamesView->setVisible(ready);
|
||||||
|
gamesStatus->setVisible(!ready);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case UserGamesModel::State::Offline:
|
||||||
|
gamesStatus->setText(tr("User is offline."));
|
||||||
|
break;
|
||||||
|
case UserGamesModel::State::Disconnected:
|
||||||
|
gamesStatus->setText(tr("Not connected to the server."));
|
||||||
|
break;
|
||||||
|
case UserGamesModel::State::Loading:
|
||||||
|
gamesStatus->setText(tr("Loading games…"));
|
||||||
|
break;
|
||||||
|
case UserGamesModel::State::Empty:
|
||||||
|
gamesStatus->setText(tr("No active games."));
|
||||||
|
break;
|
||||||
|
case UserGamesModel::State::Error:
|
||||||
|
gamesStatus->setText(tr("Could not load games."));
|
||||||
|
break;
|
||||||
|
case UserGamesModel::State::Ready: {
|
||||||
|
// Fit exactly to the number of visible rows, scroll when more than 5.
|
||||||
|
constexpr int rowH = 38; // must match PopupGameDelegate::sizeHint
|
||||||
|
constexpr int maxRows = 5;
|
||||||
|
const int count = gamesModel->rowCount();
|
||||||
|
gamesView->setFixedHeight(qMin(count, maxRows) * rowH + 2);
|
||||||
|
gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < resp.game_list_size(); ++i) {
|
|
||||||
auto *item = new QStandardItem;
|
|
||||||
item->setData(QVariant::fromValue(resp.game_list(i)), PopupRoles::GameData);
|
|
||||||
item->setEditable(false);
|
|
||||||
gamesModel->appendRow(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = gamesModel->rowCount();
|
|
||||||
const int visible = qMin(count, maxRows);
|
|
||||||
gamesView->setFixedHeight(visible * rowH + 2);
|
|
||||||
gamesView->setVerticalScrollBarPolicy(count > maxRows ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
|
|
||||||
|
|
||||||
adjustSize();
|
adjustSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserInfoPopup::refreshGames()
|
void UserInfoPopup::refreshGames()
|
||||||
{
|
{
|
||||||
gamesModel->clear();
|
|
||||||
gamesView->hide();
|
|
||||||
gamesStatus->setText(tr("Loading games…"));
|
|
||||||
gamesStatus->show();
|
|
||||||
fetchGames();
|
fetchGames();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef COCKATRICE_USER_INFO_POPUP_H
|
#ifndef COCKATRICE_USER_INFO_POPUP_H
|
||||||
#define COCKATRICE_USER_INFO_POPUP_H
|
#define COCKATRICE_USER_INFO_POPUP_H
|
||||||
|
|
||||||
|
#include "user_games_model.h"
|
||||||
#include "user_list_painter.h"
|
#include "user_list_painter.h"
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
|
@ -21,15 +22,13 @@ class QPushButton;
|
||||||
class TabSupervisor;
|
class TabSupervisor;
|
||||||
class QListView;
|
class QListView;
|
||||||
class QPoint;
|
class QPoint;
|
||||||
class QStandardItemModel;
|
|
||||||
class Response;
|
|
||||||
template <class Key, class T> class QMap;
|
template <class Key, class T> class QMap;
|
||||||
|
|
||||||
// ── Roles ─────────────────────────────────────────────────────────────────────
|
// ── Roles ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
namespace PopupRoles
|
namespace PopupRoles
|
||||||
{
|
{
|
||||||
constexpr int GameData = Qt::UserRole + 10;
|
constexpr int GameData = UserGamesModel::GameRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Popup theme
|
// Popup theme
|
||||||
|
|
@ -143,6 +142,7 @@ public:
|
||||||
void
|
void
|
||||||
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
showForUser(const QString &userName, const ServerInfo_User &userInfo, bool online, bool isBuddy, bool isIgnored);
|
||||||
void fetchGames();
|
void fetchGames();
|
||||||
|
void setOnline(bool online);
|
||||||
|
|
||||||
[[nodiscard]] QString getCurrentUser() const
|
[[nodiscard]] QString getCurrentUser() const
|
||||||
{
|
{
|
||||||
|
|
@ -199,7 +199,7 @@ protected:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void refreshGames();
|
void refreshGames();
|
||||||
void onGamesReceived(const Response &r, const QString &forUser);
|
void updateGamesDisplay();
|
||||||
void onGamesContextMenu(const QPoint &pos);
|
void onGamesContextMenu(const QPoint &pos);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -218,6 +218,8 @@ private:
|
||||||
QString currentUser;
|
QString currentUser;
|
||||||
ServerInfo_User currentUserInfo;
|
ServerInfo_User currentUserInfo;
|
||||||
bool currentOnline = false;
|
bool currentOnline = false;
|
||||||
|
bool currentBuddy = false;
|
||||||
|
bool currentIgnored = false;
|
||||||
std::function<bool(const QString &userName)> gameInviteAvailable;
|
std::function<bool(const QString &userName)> gameInviteAvailable;
|
||||||
|
|
||||||
UserInfoHeaderWidget *header;
|
UserInfoHeaderWidget *header;
|
||||||
|
|
@ -225,7 +227,7 @@ private:
|
||||||
QLabel *gamesLabel;
|
QLabel *gamesLabel;
|
||||||
QFrame *separator;
|
QFrame *separator;
|
||||||
QListView *gamesView;
|
QListView *gamesView;
|
||||||
QStandardItemModel *gamesModel;
|
UserGamesModel *gamesModel;
|
||||||
QLabel *gamesStatus;
|
QLabel *gamesStatus;
|
||||||
QPushButton *closeBtn;
|
QPushButton *closeBtn;
|
||||||
QPushButton *refreshBtn;
|
QPushButton *refreshBtn;
|
||||||
|
|
|
||||||
|
|
@ -617,7 +617,14 @@ void UserListWidget::bind(UserListManager *mgr)
|
||||||
connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup);
|
connect(manager, &UserListManager::addedToIgnoreList, this, refreshCurrentPopup);
|
||||||
connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen);
|
connect(manager, &UserListManager::removedFromIgnoreList, this, refreshIfPopupOpen);
|
||||||
connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup);
|
connect(manager, &UserListManager::userJoinedOnline, this, refreshCurrentPopup);
|
||||||
connect(manager, &UserListManager::userLeftOnline, this, refreshIfPopupOpen);
|
connect(manager, &UserListManager::userLeftOnline, this, [this, refreshIfPopupOpen](const QString &name) {
|
||||||
|
// The online row may already have been removed; the pinned popup must
|
||||||
|
// still invalidate its games request and display the offline state.
|
||||||
|
if (userInfoPopup && userInfoPopup->getCurrentUser() == name) {
|
||||||
|
userInfoPopup->setOnline(false);
|
||||||
|
}
|
||||||
|
refreshIfPopupOpen(name);
|
||||||
|
});
|
||||||
|
|
||||||
rebuild();
|
rebuild();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,18 @@ cockatrice_add_test(NAME user_data_backup_test SOURCES user_data_backup_test.cpp
|
||||||
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME password_hash_test SOURCES password_hash_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME report_categories_test SOURCES report_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
cockatrice_add_test(NAME warning_categories_test SOURCES warning_categories_test.cpp LIBS libcockatrice_utility)
|
||||||
|
cockatrice_add_test(
|
||||||
|
NAME
|
||||||
|
user_games_model_test
|
||||||
|
SOURCES
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src/interface/widgets/server/user/user_games_model.cpp
|
||||||
|
user_games_model_test.cpp
|
||||||
|
LIBS
|
||||||
|
libcockatrice_network_client_abstract
|
||||||
|
INCLUDE_DIRS
|
||||||
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
|
${CMAKE_SOURCE_DIR}/libcockatrice_network
|
||||||
|
)
|
||||||
# The installer's holder probe is Windows-only, but the wording it produces and the
|
# The installer's holder probe is Windows-only, but the wording it produces and the
|
||||||
# question which programs are the payload's own are plain C++ and are what a stuck user
|
# question which programs are the payload's own are plain C++ and are what a stuck user
|
||||||
# has to act on, so they are tested everywhere.
|
# has to act on, so they are tested everywhere.
|
||||||
|
|
|
||||||
248
tests/user_games_model_test.cpp
Normal file
248
tests/user_games_model_test.cpp
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#include "interface/widgets/server/user/user_games_model.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemModelTester>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QPersistentModelIndex>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/commands.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/server_message.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
#include <libcockatrice/rng/rng_abstract.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
RNG_Abstract *rng = nullptr; // required by the transitive server library; this suite sends no game commands
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class GamesClient : public AbstractClient
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QList<CommandContainer> sent;
|
||||||
|
|
||||||
|
GamesClient()
|
||||||
|
{
|
||||||
|
setStatus(StatusLoggedIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
~GamesClient() override
|
||||||
|
{
|
||||||
|
qDeleteAll(pendingCommands);
|
||||||
|
pendingCommands.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeStatus(ClientStatus status)
|
||||||
|
{
|
||||||
|
setStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void answer(int request, Response response)
|
||||||
|
{
|
||||||
|
response.set_cmd_id(sent.at(request).cmd_id());
|
||||||
|
ServerMessage message;
|
||||||
|
message.set_message_type(ServerMessage::RESPONSE);
|
||||||
|
*message.mutable_response() = response;
|
||||||
|
processProtocolItem(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void sendCommandContainer(const CommandContainer &command) override
|
||||||
|
{
|
||||||
|
sent.append(command);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Response gamesResponse(std::initializer_list<int> ids)
|
||||||
|
{
|
||||||
|
Response response;
|
||||||
|
response.set_response_code(Response::RespOk);
|
||||||
|
auto *games = response.MutableExtension(Response_GetGamesOfUser::ext);
|
||||||
|
for (int id : ids) {
|
||||||
|
auto *game = games->add_game_list();
|
||||||
|
game->set_game_id(id);
|
||||||
|
game->set_room_id(7);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
using State = UserGamesModel::State;
|
||||||
|
|
||||||
|
TEST(UserGamesModel, OfflineUserDoesNotSendARequest)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", false);
|
||||||
|
model.refresh();
|
||||||
|
EXPECT_TRUE(client.sent.isEmpty());
|
||||||
|
EXPECT_EQ(State::Offline, model.getState());
|
||||||
|
EXPECT_FALSE(model.canRefresh());
|
||||||
|
EXPECT_EQ(0, model.rowCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, PresenceLossInvalidatesTheOutstandingResponse)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
EXPECT_EQ(State::Loading, model.getState());
|
||||||
|
EXPECT_FALSE(model.canRefresh());
|
||||||
|
model.setUser("Alice", false);
|
||||||
|
client.answer(0, gamesResponse({1}));
|
||||||
|
EXPECT_EQ(State::Offline, model.getState());
|
||||||
|
EXPECT_EQ(0, model.rowCount());
|
||||||
|
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client.answer(1, gamesResponse({2}));
|
||||||
|
EXPECT_EQ(State::Ready, model.getState());
|
||||||
|
EXPECT_TRUE(model.canRefresh());
|
||||||
|
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, SwitchingBackToTheSameUserDoesNotAcceptTheirOldRequest)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
model.setUser("Bob", true);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client.answer(2, gamesResponse({3}));
|
||||||
|
client.answer(0, gamesResponse({1}));
|
||||||
|
client.answer(1, gamesResponse({2}));
|
||||||
|
EXPECT_EQ(1, model.rowCount());
|
||||||
|
EXPECT_EQ(3, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, RefreshSupersedesThePreviousRequest)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
model.refresh();
|
||||||
|
client.answer(0, gamesResponse({1}));
|
||||||
|
EXPECT_EQ(State::Loading, model.getState());
|
||||||
|
EXPECT_EQ(0, model.rowCount());
|
||||||
|
client.answer(1, gamesResponse({2}));
|
||||||
|
EXPECT_EQ(State::Ready, model.getState());
|
||||||
|
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, EmptySuccessAndMissingPayloadAreDifferentStates)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client.answer(0, gamesResponse({}));
|
||||||
|
EXPECT_EQ(State::Empty, model.getState());
|
||||||
|
EXPECT_TRUE(model.canRefresh());
|
||||||
|
|
||||||
|
model.refresh();
|
||||||
|
Response missingPayload;
|
||||||
|
missingPayload.set_response_code(Response::RespOk);
|
||||||
|
client.answer(1, missingPayload);
|
||||||
|
EXPECT_EQ(State::Error, model.getState());
|
||||||
|
EXPECT_TRUE(model.canRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, FailedRequestsCanBeRetried)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
Response error;
|
||||||
|
error.set_response_code(Response::RespNameNotFound);
|
||||||
|
client.answer(0, error);
|
||||||
|
EXPECT_EQ(State::Error, model.getState());
|
||||||
|
model.refresh();
|
||||||
|
client.answer(1, gamesResponse({4}));
|
||||||
|
EXPECT_EQ(State::Ready, model.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, DisconnectRejectsLateRepliesAndReconnectFetchesAgain)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client.changeStatus(StatusDisconnecting);
|
||||||
|
client.answer(0, gamesResponse({1}));
|
||||||
|
EXPECT_EQ(State::Disconnected, model.getState());
|
||||||
|
EXPECT_FALSE(model.canRefresh());
|
||||||
|
model.refresh();
|
||||||
|
EXPECT_EQ(1, client.sent.size());
|
||||||
|
client.changeStatus(StatusLoggedIn);
|
||||||
|
client.answer(1, gamesResponse({2}));
|
||||||
|
EXPECT_EQ(State::Ready, model.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, ClientDestructionClearsGamesAndDisablesRefresh)
|
||||||
|
{
|
||||||
|
auto client = std::make_unique<GamesClient>();
|
||||||
|
UserGamesModel model(client.get());
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client->answer(0, gamesResponse({1}));
|
||||||
|
client.reset();
|
||||||
|
EXPECT_EQ(State::Disconnected, model.getState());
|
||||||
|
EXPECT_EQ(0, model.rowCount());
|
||||||
|
EXPECT_FALSE(model.canRefresh());
|
||||||
|
model.refresh();
|
||||||
|
EXPECT_EQ(State::Disconnected, model.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, ResetInvalidatesAnOpenGameActionAndPreservesTheModelContract)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::Fatal);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
client.answer(0, gamesResponse({1, 2, 3}));
|
||||||
|
QPersistentModelIndex selected(model.index(1));
|
||||||
|
EXPECT_TRUE(selected.isValid());
|
||||||
|
model.refresh();
|
||||||
|
EXPECT_FALSE(selected.isValid());
|
||||||
|
EXPECT_EQ(0, model.rowCount());
|
||||||
|
client.answer(1, gamesResponse({4}));
|
||||||
|
EXPECT_EQ(1, model.rowCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, StateListenerCanSelectAnotherUserBeforeTheRequestIsSent)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client);
|
||||||
|
bool switched = false;
|
||||||
|
QObject::connect(&model, &UserGamesModel::stateChanged, &model, [&] {
|
||||||
|
if (!switched && model.getState() == State::Loading) {
|
||||||
|
switched = true;
|
||||||
|
model.setUser("Bob", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
EXPECT_TRUE(client.sent.isEmpty());
|
||||||
|
EXPECT_EQ(State::Offline, model.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UserGamesModel, TimedOutResponseCannotReplaceARetry)
|
||||||
|
{
|
||||||
|
GamesClient client;
|
||||||
|
UserGamesModel model(&client, 50);
|
||||||
|
model.setUser("Alice", true);
|
||||||
|
QSignalSpy changes(&model, &UserGamesModel::stateChanged);
|
||||||
|
ASSERT_TRUE(changes.wait(1000));
|
||||||
|
ASSERT_EQ(State::Error, model.getState());
|
||||||
|
EXPECT_TRUE(model.canRefresh());
|
||||||
|
model.refresh();
|
||||||
|
client.answer(0, gamesResponse({1}));
|
||||||
|
EXPECT_EQ(State::Loading, model.getState());
|
||||||
|
client.answer(1, gamesResponse({2}));
|
||||||
|
EXPECT_EQ(State::Ready, model.getState());
|
||||||
|
EXPECT_EQ(2, model.data(model.index(0), UserGamesModel::GameRole).value<ServerInfo_Game>().game_id());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue