[Move refactor] Reparent orphan classes (#6236)

* Move orphaned classes to their correct parent folders.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2025-10-09 14:15:19 +02:00 committed by GitHub
parent 1ef07309d6
commit d9c65d4ae0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
143 changed files with 171 additions and 169 deletions

View file

@ -0,0 +1,638 @@
#include "chat_view.h"
#include "../../client/sound_engine.h"
#include "../../interface/pixel_map_generator.h"
#include "../../tabs/tab_account.h"
#include "../user/user_context_menu.h"
#include "../user/user_list_manager.h"
#include "../user/user_list_proxy.h"
#include <QApplication>
#include <QDateTime>
#include <QDesktopServices>
#include <QMouseEvent>
#include <QScrollBar>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/settings/cache_settings.h>
const QColor DEFAULT_MENTION_COLOR = QColor(194, 31, 47);
UserMessagePosition::UserMessagePosition(QTextCursor &cursor)
{
block = cursor.block();
relativePosition = cursor.position() - block.position();
}
ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _showTimestamps, QWidget *parent)
: QTextBrowser(parent), tabSupervisor(_tabSupervisor), game(_game),
userListProxy(_tabSupervisor->getUserListManager()), evenNumber(true), showTimestamps(_showTimestamps),
hoveredItemType(HoveredNothing)
{
if (palette().windowText().color().lightness() > 200) {
document()->setDefaultStyleSheet(R"(
a { text-decoration: none; color: rgb(71,158,252); }
.blue { color: rgb(71,158,252); }
)");
serverMessageColor = QColor(0xFF, 0x73, 0x83);
otherUserColor = otherUserColor.lighter(150);
linkColor = QColor(71, 158, 252);
} else {
document()->setDefaultStyleSheet(R"(
a { text-decoration: none; color: blue; }
.blue { color: blue }
)");
linkColor = palette().link().color();
}
userContextMenu = new UserContextMenu(tabSupervisor, this, game);
connect(userContextMenu, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
ownUserName = userListProxy->getOwnUsername();
mention = "@" + ownUserName;
mentionFormat.setFontWeight(QFont::Bold);
mentionFormatOtherUser.setFontWeight(QFont::Bold);
mentionFormatOtherUser.setForeground(linkColor);
mentionFormatOtherUser.setAnchor(true);
viewport()->setCursor(Qt::IBeamCursor);
setReadOnly(true);
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
setOpenLinks(false);
connect(this, &ChatView::anchorClicked, this, &ChatView::openLink);
}
void ChatView::retranslateUi()
{
userContextMenu->retranslateUi();
}
QTextCursor ChatView::prepareBlock(bool same)
{
lastSender.clear();
QTextCursor cursor(document());
cursor.movePosition(QTextCursor::End);
if (same) {
cursor.insertHtml("<br>");
} else {
QTextBlockFormat blockFormat;
if ((evenNumber = !evenNumber))
blockFormat.setBackground(palette().window());
else
blockFormat.setBackground(palette().base());
blockFormat.setForeground(palette().text());
blockFormat.setBottomMargin(4);
cursor.insertBlock(blockFormat);
}
return cursor;
}
void ChatView::appendHtml(const QString &html)
{
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
prepareBlock().insertHtml(html);
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::appendHtmlServerMessage(const QString &html, bool optionalIsBold, QString optionalFontColor)
{
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
QString htmlText =
"<font color=" + ((optionalFontColor.size() > 0) ? optionalFontColor : serverMessageColor.name()) + ">" +
QDateTime::currentDateTime().toString("[hh:mm:ss] ") + html + "</font>";
if (optionalIsBold)
htmlText = "<b>" + htmlText + "</b>";
prepareBlock().insertHtml(htmlText);
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::appendCardTag(QTextCursor &cursor, const QString &cardName)
{
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat anchorFormat = oldFormat;
anchorFormat.setForeground(linkColor);
anchorFormat.setAnchor(true);
anchorFormat.setAnchorHref("card://" + cardName);
anchorFormat.setFontItalic(true);
cursor.setCharFormat(anchorFormat);
cursor.insertText(cardName);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendUrlTag(QTextCursor &cursor, QString url)
{
if (!url.contains("://"))
url.prepend("https://");
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat anchorFormat = oldFormat;
anchorFormat.setForeground(linkColor);
anchorFormat.setAnchor(true);
anchorFormat.setAnchorHref(url);
anchorFormat.setUnderlineColor(linkColor);
anchorFormat.setFontUnderline(true);
cursor.setCharFormat(anchorFormat);
cursor.insertText(url);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendMessage(QString message,
RoomMessageTypeFlags messageType,
const ServerInfo_User &userInfo,
bool playerBold)
{
const QString userName = QString::fromStdString(userInfo.name());
const UserLevelFlags userLevel = UserLevelFlags(userInfo.user_level());
bool atBottom = verticalScrollBar()->value() >= verticalScrollBar()->maximum();
// messageType should be Event_RoomSay::UserMessage though we don't actually check
bool isUserMessage = !(userName.toLower() == "servatrice" || userName.isEmpty());
bool sameSender = isUserMessage && userName == lastSender;
QTextCursor cursor = prepareBlock(sameSender);
lastSender = userName;
// timestamp
if (showTimestamps && ((!sameSender && isUserMessage) || userName.toLower() == "servatrice")) {
QTextCharFormat timeFormat;
timeFormat.setForeground(serverMessageColor);
timeFormat.setFontWeight(QFont::Bold);
cursor.setCharFormat(timeFormat);
cursor.insertText(QDateTime::currentDateTime().toString("[hh:mm:ss] "));
}
// nickname
if (isUserMessage) {
QTextCharFormat senderFormat;
if (userName == ownUserName) {
senderFormat.setForeground(QBrush(getCustomMentionColor()));
senderFormat.setFontWeight(QFont::Bold);
} else {
senderFormat.setForeground(QBrush(otherUserColor));
if (playerBold) {
senderFormat.setFontWeight(QFont::Bold);
}
}
senderFormat.setAnchor(true);
senderFormat.setAnchorHref("user://" + QString::number(userLevel) + "_" + userName);
if (sameSender) {
cursor.insertText(" ");
} else {
const int pixelSize = QFontInfo(cursor.charFormat().font()).pixelSize();
bool isBuddy = userListProxy->isUserBuddy(userName);
const QString privLevel = userInfo.has_privlevel() ? QString::fromStdString(userInfo.privlevel()) : "NONE";
cursor.insertImage(UserLevelPixmapGenerator::generatePixmap(pixelSize, userLevel, userInfo.pawn_colors(),
isBuddy, privLevel)
.toImage());
cursor.insertText(" ");
cursor.setCharFormat(senderFormat);
cursor.insertText(userName);
cursor.insertText(": ");
userMessagePositions[userName].append(cursor);
}
}
// use different color for server messages
defaultFormat = QTextCharFormat();
if (!isUserMessage) {
if (messageType == Event_RoomSay::ChatHistory) {
defaultFormat.setForeground(Qt::gray); // FIXME : hardcoded color
defaultFormat.setFontWeight(QFont::Light);
defaultFormat.setFontItalic(true);
static const QRegularExpression userNameRegex("^(\\[[^\\]]*\\]\\s)(\\S+):\\s");
auto match = userNameRegex.match(message);
if (match.hasMatch()) {
cursor.setCharFormat(defaultFormat);
UserMessagePosition pos(cursor);
pos.relativePosition = match.captured(0).length(); // set message start
auto before = match.captured(1);
auto sentBy = match.captured(2);
cursor.insertText(before); // add message timestamp
QTextCharFormat senderFormat(defaultFormat);
senderFormat.setAnchor(true);
// this underscore is important, it is used to add the user level, but in this case the level is
// unknown, if the name contains an underscore it would split up the name
senderFormat.setAnchorHref("user://_" + sentBy);
cursor.setCharFormat(senderFormat);
cursor.insertText(sentBy); // add username with href so it shows the menu
userMessagePositions[sentBy].append(pos); // save message position
message.remove(0, pos.relativePosition - 2); // do not remove semicolon
}
} else {
defaultFormat.setForeground(Qt::darkGreen); // FIXME : hardcoded color
defaultFormat.setFontWeight(QFont::Bold);
}
}
cursor.setCharFormat(defaultFormat);
bool mentionEnabled = SettingsCache::instance().getChatMention();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', Qt::SkipEmptyParts);
#else
highlightedWords = SettingsCache::instance().getHighlightWords().split(' ', QString::SkipEmptyParts);
#endif
// parse the message
while (message.size()) {
QChar c = message.at(0);
switch (c.toLatin1()) {
case '[':
checkTag(cursor, message);
break;
case '@':
if (mentionEnabled) {
checkMention(cursor, message, userName, userLevel);
} else {
cursor.insertText(c, defaultFormat);
message = message.mid(1);
}
break;
case ' ':
cursor.insertText(c, defaultFormat);
message = message.mid(1);
break;
default:
if (c.isLetterOrNumber()) {
checkWord(cursor, message);
} else {
cursor.insertText(c, defaultFormat);
message = message.mid(1);
}
break;
}
}
if (atBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void ChatView::checkTag(QTextCursor &cursor, QString &message)
{
if (message.startsWith("[card]")) {
message = message.mid(6);
int closeTagIndex = message.indexOf("[/card]");
QString cardName = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 7);
appendCardTag(cursor, cardName);
return;
}
if (message.startsWith("[[")) {
message = message.mid(2);
int closeTagIndex = message.indexOf("]]");
QString cardName = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 2);
appendCardTag(cursor, cardName);
return;
}
if (message.startsWith("[url]")) {
message = message.mid(5);
int closeTagIndex = message.indexOf("[/url]");
QString url = message.left(closeTagIndex);
if (closeTagIndex == -1)
message.clear();
else
message = message.mid(closeTagIndex + 6);
appendUrlTag(cursor, url);
return;
}
// no valid tag found
checkWord(cursor, message);
}
void ChatView::checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel)
{
const static auto notALetterOrNumber = QRegularExpression("[^a-zA-Z0-9]");
int firstSpace = message.indexOf(' ');
QString fullMentionUpToSpaceOrEnd = (firstSpace == -1) ? message.mid(1) : message.mid(1, firstSpace - 1);
QString mentionIntact = fullMentionUpToSpaceOrEnd;
while (fullMentionUpToSpaceOrEnd.size()) {
const ServerInfo_User *onlineUser = userListProxy->getOnlineUser(fullMentionUpToSpaceOrEnd);
if (onlineUser) // Is there a user online named this?
{
if (ownUserName.toLower() == fullMentionUpToSpaceOrEnd.toLower()) // Is this user you?
{
// You have received a valid mention!!
soundEngine->playSound("chat_mention");
mentionFormat.setBackground(QBrush(getCustomMentionColor()));
mentionFormat.setForeground(SettingsCache::instance().getChatMentionForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText(mention, mentionFormat);
message = message.mid(mention.size());
showSystemPopup(userName);
} else {
QString correctUserName = QString::fromStdString(onlineUser->name());
mentionFormatOtherUser.setAnchorHref("user://" + QString::number(onlineUser->user_level()) + "_" +
correctUserName);
cursor.insertText("@" + correctUserName, mentionFormatOtherUser);
message = message.mid(correctUserName.size() + 1);
}
cursor.setCharFormat(defaultFormat);
return;
}
if (isModeratorSendingGlobal(userLevel, fullMentionUpToSpaceOrEnd)) {
// Moderator Sending Global Message
soundEngine->playSound("all_mention");
mentionFormat.setBackground(QBrush(getCustomMentionColor()));
mentionFormat.setForeground(SettingsCache::instance().getChatMentionForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText("@" + fullMentionUpToSpaceOrEnd, mentionFormat);
message = message.mid(fullMentionUpToSpaceOrEnd.size() + 1);
showSystemPopup(userName);
cursor.setCharFormat(defaultFormat);
return;
}
if (fullMentionUpToSpaceOrEnd.right(1).indexOf(notALetterOrNumber) == -1 ||
fullMentionUpToSpaceOrEnd.size() < 2) {
cursor.insertText("@" + mentionIntact, defaultFormat);
message = message.mid(mentionIntact.size() + 1);
cursor.setCharFormat(defaultFormat);
return;
}
fullMentionUpToSpaceOrEnd.chop(1);
}
// no valid mention found
checkWord(cursor, message);
}
void ChatView::checkWord(QTextCursor &cursor, QString &message)
{
// extract the first word
QString rest;
QString fullWordUpToSpaceOrEnd = extractNextWord(message, rest);
// check urls
if (fullWordUpToSpaceOrEnd.startsWith("http://", Qt::CaseInsensitive) ||
fullWordUpToSpaceOrEnd.startsWith("https://", Qt::CaseInsensitive) ||
fullWordUpToSpaceOrEnd.startsWith("www.", Qt::CaseInsensitive)) {
QUrl qUrl(fullWordUpToSpaceOrEnd);
if (qUrl.isValid()) {
appendUrlTag(cursor, fullWordUpToSpaceOrEnd);
cursor.insertText(rest, defaultFormat);
return;
}
}
// check word mentions
for (const QString &word : highlightedWords) {
if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) {
// You have received a valid mention of custom word!!
highlightFormat.setBackground(QBrush(getCustomHighlightColor()));
highlightFormat.setForeground(SettingsCache::instance().getChatHighlightForeground() ? QBrush(Qt::white)
: QBrush(Qt::black));
cursor.insertText(fullWordUpToSpaceOrEnd, highlightFormat);
cursor.insertText(rest, defaultFormat);
QApplication::alert(this);
return;
}
}
// not a special word; just print it
cursor.insertText(fullWordUpToSpaceOrEnd + rest, defaultFormat);
}
QString ChatView::extractNextWord(QString &message, QString &rest)
{
// get the first next space and extract the word
QString word;
int firstSpace = message.indexOf(' ');
if (firstSpace == -1) {
word = message;
message.clear();
} else {
word = message.mid(0, firstSpace);
message = message.mid(firstSpace);
}
// remove any punctuation from the end and pass it separately
for (int len = word.size() - 1; len >= 0; --len) {
if (word.at(len).isLetterOrNumber()) {
rest = word.mid(len + 1);
return word.mid(0, len + 1);
}
}
rest = word;
return QString();
}
bool ChatView::isModeratorSendingGlobal(QFlags<ServerInfo_User::UserLevelFlag> userLevelFlag, QString message)
{
int userLevel = QString::number(userLevelFlag).toInt();
QStringList getAttentionList;
getAttentionList << "/all"; // Send a message to all users
return (getAttentionList.contains(message) &&
(userLevel & ServerInfo_User::IsModerator || userLevel & ServerInfo_User::IsAdmin));
}
void ChatView::actMessageClicked()
{
emit messageClickedSignal();
}
void ChatView::showSystemPopup(const QString &userName)
{
QApplication::alert(this);
if (SettingsCache::instance().getShowMentionPopup()) {
emit showMentionPopup(userName);
}
}
QColor ChatView::getCustomMentionColor()
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
QColor customColor = QColor::fromString("#" + SettingsCache::instance().getChatMentionColor());
#else
QColor customColor;
customColor.setNamedColor("#" + SettingsCache::instance().getChatMentionColor());
#endif
return customColor.isValid() ? customColor : DEFAULT_MENTION_COLOR;
}
QColor ChatView::getCustomHighlightColor()
{
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
QColor customColor = QColor::fromString("#" + SettingsCache::instance().getChatMentionColor());
#else
QColor customColor;
customColor.setNamedColor("#" + SettingsCache::instance().getChatMentionColor());
#endif
return customColor.isValid() ? customColor : DEFAULT_MENTION_COLOR;
}
void ChatView::clearChat()
{
document()->clear();
lastSender = "";
evenNumber = true;
}
void ChatView::redactMessages(const QString &userName, int amount)
{
auto &messagePositions = userMessagePositions[userName];
bool removedLastMessage = false;
QTextCursor cursor(document());
for (; !messagePositions.isEmpty() && amount != 0; --amount) {
auto position = messagePositions.takeLast(); // go backwards from last message
cursor.setPosition(position.block.position()); // move to start of block, then continue to start of message
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, position.relativePosition);
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); // select until end of block
cursor.removeSelectedText();
// if the cursor is at the end of the text it is possible to add text to this block still
removedLastMessage |= cursor.atEnd();
// we will readd this position later
}
if (removedLastMessage) {
cursor.movePosition(QTextCursor::End);
messagePositions.append(cursor);
// note that this message might stay empty, this is not harmful as it will simply remove nothing the next time
}
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void ChatView::enterEvent(QEnterEvent * /*event*/)
#else
void ChatView::enterEvent(QEvent * /*event*/)
#endif
{
setMouseTracking(true);
}
void ChatView::leaveEvent(QEvent * /*event*/)
{
setMouseTracking(false);
}
QTextFragment ChatView::getFragmentUnderMouse(const QPoint &pos) const
{
QTextCursor cursor(cursorForPosition(pos));
QTextBlock block(cursor.block());
QTextBlock::iterator it;
for (it = block.begin(); !(it.atEnd()); ++it) {
QTextFragment frag = it.fragment();
if (frag.contains(cursor.position()))
return frag;
}
return QTextFragment();
}
void ChatView::mouseMoveEvent(QMouseEvent *event)
{
QString anchorHref = getFragmentUnderMouse(event->pos()).charFormat().anchorHref();
if (!anchorHref.isEmpty()) {
const int delimiterIndex = anchorHref.indexOf("://");
if (delimiterIndex != -1) {
const QString scheme = anchorHref.left(delimiterIndex);
hoveredContent = anchorHref.mid(delimiterIndex + 3);
if (scheme == "card") {
hoveredItemType = HoveredCard;
emit cardNameHovered(hoveredContent);
} else if (scheme == "user")
hoveredItemType = HoveredUser;
else
hoveredItemType = HoveredUrl;
viewport()->setCursor(Qt::PointingHandCursor);
} else {
hoveredItemType = HoveredNothing;
viewport()->setCursor(Qt::IBeamCursor);
}
} else {
hoveredItemType = HoveredNothing;
viewport()->setCursor(Qt::IBeamCursor);
}
QTextBrowser::mouseMoveEvent(event);
}
void ChatView::mousePressEvent(QMouseEvent *event)
{
switch (hoveredItemType) {
case HoveredCard: {
if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton))
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
emit showCardInfoPopup(event->globalPosition().toPoint(), {hoveredContent});
#else
emit showCardInfoPopup(event->globalPos(), {hoveredContent});
#endif
break;
}
case HoveredUser: {
if (event->button() != Qt::MiddleButton) {
const int delimiterIndex = hoveredContent.indexOf("_");
const QString userName = hoveredContent.mid(delimiterIndex + 1);
switch (event->button()) {
case Qt::RightButton: {
UserLevelFlags userLevel(hoveredContent.left(delimiterIndex).toInt());
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
userContextMenu->showContextMenu(event->globalPosition().toPoint(), userName, userLevel, this);
#else
userContextMenu->showContextMenu(event->globalPos(), userName, userLevel, this);
#endif
break;
}
case Qt::LeftButton: {
if (event->modifiers() == Qt::ControlModifier) {
emit openMessageDialog(userName, true);
} else
emit addMentionTag("@" + userName);
break;
}
default:
break;
}
}
break;
}
default: {
QTextBrowser::mousePressEvent(event);
}
}
}
void ChatView::mouseReleaseEvent(QMouseEvent *event)
{
if ((event->button() == Qt::MiddleButton) || (event->button() == Qt::LeftButton))
emit deleteCardInfoPopup(QString("_"));
QTextBrowser::mouseReleaseEvent(event);
}
void ChatView::openLink(const QUrl &link)
{
if ((link.scheme() == "card") || (link.scheme() == "user"))
return;
QDesktopServices::openUrl(link);
}

View file

@ -0,0 +1,126 @@
/**
* @file chat_view.h
* @ingroup NetworkingWidgets
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef CHATVIEW_H
#define CHATVIEW_H
#include "../../tabs/tab_supervisor.h"
#include "../user/user_list_widget.h"
#include <QAction>
#include <QColor>
#include <QTextBrowser>
#include <QTextCursor>
#include <QTextFragment>
#include <libcockatrice/network/server/remote/room_message_type.h>
#include <libcockatrice/network/server/remote/user_level.h>
class AbstractGame;
class QTextTable;
class QMouseEvent;
class UserContextMenu;
class UserListProxy;
class UserMessagePosition
{
public:
#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
UserMessagePosition() = default; // older qt versions require a default constructor to use in containers
#endif
UserMessagePosition(QTextCursor &cursor);
int relativePosition;
QTextBlock block;
};
class ChatView : public QTextBrowser
{
Q_OBJECT
protected:
TabSupervisor *const tabSupervisor;
AbstractGame *const game;
private:
enum HoveredItemType
{
HoveredNothing,
HoveredUrl,
HoveredCard,
HoveredUser
};
const UserListProxy *const userListProxy;
UserContextMenu *userContextMenu;
QString lastSender;
QString ownUserName;
QString mention;
QTextCharFormat mentionFormat;
QTextCharFormat highlightFormat;
QTextCharFormat mentionFormatOtherUser;
QTextCharFormat defaultFormat;
QStringList highlightedWords;
bool evenNumber;
bool showTimestamps;
HoveredItemType hoveredItemType;
QString hoveredContent;
QAction *messageClicked;
QMap<QString, QVector<UserMessagePosition>> userMessagePositions;
QTextFragment getFragmentUnderMouse(const QPoint &pos) const;
QTextCursor prepareBlock(bool same = false);
void appendCardTag(QTextCursor &cursor, const QString &cardName);
void appendUrlTag(QTextCursor &cursor, QString url);
static QColor getCustomMentionColor();
static QColor getCustomHighlightColor();
void showSystemPopup(const QString &userName);
bool isModeratorSendingGlobal(QFlags<ServerInfo_User::UserLevelFlag> userLevelFlag, QString message);
void checkTag(QTextCursor &cursor, QString &message);
void checkMention(QTextCursor &cursor, QString &message, const QString &userName, UserLevelFlags userLevel);
void checkWord(QTextCursor &cursor, QString &message);
QString extractNextWord(QString &message, QString &rest);
QColor otherUserColor = QColor(0, 65, 255); // dark blue
QColor serverMessageColor = QColor(0x85, 0x15, 0x15);
QColor linkColor;
private slots:
void openLink(const QUrl &link);
void actMessageClicked();
public:
ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _showTimestamps, QWidget *parent = nullptr);
void retranslateUi();
void appendHtml(const QString &html);
void virtual appendHtmlServerMessage(const QString &html,
bool optionalIsBold = false,
QString optionalFontColor = QString());
void appendMessage(QString message,
RoomMessageTypeFlags messageType = {},
const ServerInfo_User &userInfo = {},
bool playerBold = false);
void clearChat();
void redactMessages(const QString &userName, int amount);
protected:
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
signals:
void openMessageDialog(const QString &userName, bool focus);
void cardNameHovered(QString cardName);
void showCardInfoPopup(const QPoint &pos, const CardRef &cardRef);
void deleteCardInfoPopup(QString cardName);
void addMentionTag(QString mentionTag);
void messageClickedSignal();
void showMentionPopup(const QString &userName);
};
#endif

View file

@ -0,0 +1,393 @@
#include "game_selector.h"
#include "../dialogs/dlg_create_game.h"
#include "../dialogs/dlg_filter_games.h"
#include "../interface/widgets/utility/get_text_with_max.h"
#include "../tabs/tab_account.h"
#include "../tabs/tab_game.h"
#include "../tabs/tab_room.h"
#include "../tabs/tab_supervisor.h"
#include "games_model.h"
#include "user/user_list_manager.h"
#include <QApplication>
#include <QCheckBox>
#include <QDebug>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QTreeView>
#include <QVBoxLayout>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h>
GameSelector::GameSelector(AbstractClient *_client,
TabSupervisor *_tabSupervisor,
TabRoom *_room,
const QMap<int, QString> &_rooms,
const QMap<int, GameTypeMap> &_gameTypes,
const bool restoresettings,
const bool _showfilters,
QWidget *parent)
: QGroupBox(parent), client(_client), tabSupervisor(_tabSupervisor), room(_room), showFilters(_showfilters)
{
gameListView = new QTreeView;
gameListView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(gameListView, &QTreeView::customContextMenuRequested, this, &GameSelector::customContextMenu);
gameListModel = new GamesModel(_rooms, _gameTypes, this);
if (showFilters) {
gameListProxyModel = new GamesProxyModel(this, tabSupervisor->getUserListManager());
gameListProxyModel->setSourceModel(gameListModel);
gameListProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
gameListView->setModel(gameListProxyModel);
} else {
gameListView->setModel(gameListModel);
}
gameListView->setIconSize(QSize(13, 13));
gameListView->setSortingEnabled(true);
gameListView->sortByColumn(gameListModel->startTimeColIndex(), Qt::AscendingOrder);
gameListView->setAlternatingRowColors(true);
gameListView->setRootIsDecorated(true);
// game created width
gameListView->setColumnWidth(1, gameListView->columnWidth(2) * 0.7);
// players width
gameListView->resizeColumnToContents(6);
// description width
gameListView->setColumnWidth(2, gameListView->columnWidth(2) * 1.7);
// creator width
gameListView->setColumnWidth(3, gameListView->columnWidth(3) * 1.2);
// game type width
gameListView->setColumnWidth(4, gameListView->columnWidth(4) * 1.4);
if (_room)
gameListView->header()->hideSection(gameListModel->roomColIndex());
if (room)
gameTypeMap = gameListModel->getGameTypes().value(room->getRoomId());
if (showFilters && restoresettings)
gameListProxyModel->loadFilterParameters(gameTypeMap);
gameListView->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
filterButton = new QPushButton;
filterButton->setIcon(QPixmap("theme:icons/search"));
connect(filterButton, &QPushButton::clicked, this, &GameSelector::actSetFilter);
clearFilterButton = new QPushButton;
clearFilterButton->setIcon(QPixmap("theme:icons/clearsearch"));
bool filtersSetToDefault = showFilters && gameListProxyModel->areFilterParametersSetToDefaults();
clearFilterButton->setEnabled(!filtersSetToDefault);
connect(clearFilterButton, &QPushButton::clicked, this, &GameSelector::actClearFilter);
if (room) {
createButton = new QPushButton;
connect(createButton, &QPushButton::clicked, this, &GameSelector::actCreate);
} else {
createButton = nullptr;
}
joinButton = new QPushButton;
spectateButton = new QPushButton;
QHBoxLayout *buttonLayout = new QHBoxLayout;
if (showFilters) {
buttonLayout->addWidget(filterButton);
buttonLayout->addWidget(clearFilterButton);
}
buttonLayout->addStretch();
if (room)
buttonLayout->addWidget(createButton);
buttonLayout->addWidget(joinButton);
buttonLayout->addWidget(spectateButton);
buttonLayout->setAlignment(Qt::AlignTop);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(gameListView);
mainLayout->addLayout(buttonLayout);
retranslateUi();
setLayout(mainLayout);
setMinimumWidth((qreal)(gameListView->columnWidth(0) * gameListModel->columnCount()) / 1.5);
setMinimumHeight(200);
connect(joinButton, &QPushButton::clicked, this, &GameSelector::actJoin);
connect(spectateButton, &QPushButton::clicked, this, &GameSelector::actSpectate);
connect(gameListView->selectionModel(), &QItemSelectionModel::currentRowChanged, this,
&GameSelector::actSelectedGameChanged);
connect(gameListView, &QTreeView::activated, this, &GameSelector::actJoin);
connect(client, &AbstractClient::ignoreListReceived, this, &GameSelector::ignoreListReceived);
connect(client, &AbstractClient::addToListEventReceived, this, &GameSelector::processAddToListEvent);
connect(client, &AbstractClient::removeFromListEventReceived, this, &GameSelector::processRemoveFromListEvent);
}
void GameSelector::ignoreListReceived(const QList<ServerInfo_User> &)
{
gameListProxyModel->refresh();
}
void GameSelector::processAddToListEvent(const Event_AddToList &event)
{
if (event.list_name() == "ignore") {
gameListProxyModel->refresh();
}
updateTitle();
}
void GameSelector::processRemoveFromListEvent(const Event_RemoveFromList &event)
{
if (event.list_name() == "ignore") {
gameListProxyModel->refresh();
}
updateTitle();
}
void GameSelector::actSetFilter()
{
DlgFilterGames dlg(gameTypeMap, gameListProxyModel, this);
if (!dlg.exec())
return;
gameListProxyModel->setHideBuddiesOnlyGames(dlg.getHideBuddiesOnlyGames());
gameListProxyModel->setHideFullGames(dlg.getHideFullGames());
gameListProxyModel->setHideGamesThatStarted(dlg.getHideGamesThatStarted());
gameListProxyModel->setHidePasswordProtectedGames(dlg.getHidePasswordProtectedGames());
gameListProxyModel->setHideIgnoredUserGames(dlg.getHideIgnoredUserGames());
gameListProxyModel->setHideNotBuddyCreatedGames(dlg.getHideNotBuddyCreatedGames());
gameListProxyModel->setHideOpenDecklistGames(dlg.getHideOpenDecklistGames());
gameListProxyModel->setGameNameFilter(dlg.getGameNameFilter());
gameListProxyModel->setCreatorNameFilter(dlg.getCreatorNameFilter());
gameListProxyModel->setGameTypeFilter(dlg.getGameTypeFilter());
gameListProxyModel->setMaxPlayersFilter(dlg.getMaxPlayersFilterMin(), dlg.getMaxPlayersFilterMax());
gameListProxyModel->setMaxGameAge(dlg.getMaxGameAge());
gameListProxyModel->setShowOnlyIfSpectatorsCanWatch(dlg.getShowOnlyIfSpectatorsCanWatch());
gameListProxyModel->setShowSpectatorPasswordProtected(dlg.getShowSpectatorPasswordProtected());
gameListProxyModel->setShowOnlyIfSpectatorsCanChat(dlg.getShowOnlyIfSpectatorsCanChat());
gameListProxyModel->setShowOnlyIfSpectatorsCanSeeHands(dlg.getShowOnlyIfSpectatorsCanSeeHands());
gameListProxyModel->saveFilterParameters(gameTypeMap);
clearFilterButton->setEnabled(!gameListProxyModel->areFilterParametersSetToDefaults());
updateTitle();
}
void GameSelector::actClearFilter()
{
clearFilterButton->setEnabled(false);
gameListProxyModel->resetFilterParameters();
gameListProxyModel->saveFilterParameters(gameTypeMap);
updateTitle();
}
void GameSelector::actCreate()
{
if (room == nullptr) {
qWarning() << "Attempted to create game, but the room was null";
return;
}
DlgCreateGame dlg(room, room->getGameTypes(), this);
dlg.exec();
updateTitle();
}
void GameSelector::checkResponse(const Response &response)
{
// NB: We re-enable buttons for the currently selected game, which may not
// be the same game as the one for which we are processing a response.
// This could lead to situations where we join a game, select a different
// game, join the new game, then receive the response for the original
// join, which would re-activate the buttons for the second game too soon.
// However, that is better than doing things the other ways, because then
// the response to the first game could lock us out of join/join as
// spectator for the second game!
//
// Ideally we should have a way to figure out if the current game is the
// same as the one we are getting a response for, but it's probably not
// worth the trouble.
enableButtons();
switch (response.response_code()) {
case Response::RespNotInRoom:
QMessageBox::critical(this, tr("Error"), tr("Please join the appropriate room first."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("Wrong password."));
break;
case Response::RespSpectatorsNotAllowed:
QMessageBox::critical(this, tr("Error"), tr("Spectators are not allowed in this game."));
break;
case Response::RespGameFull:
QMessageBox::critical(this, tr("Error"), tr("The game is already full."));
break;
case Response::RespNameNotFound:
QMessageBox::critical(this, tr("Error"), tr("The game does not exist any more."));
break;
case Response::RespUserLevelTooLow:
QMessageBox::critical(this, tr("Error"), tr("This game is only open to registered users."));
break;
case Response::RespOnlyBuddies:
QMessageBox::critical(this, tr("Error"), tr("This game is only open to its creator's buddies."));
break;
case Response::RespInIgnoreList:
QMessageBox::critical(this, tr("Error"), tr("You are being ignored by the creator of this game."));
break;
default:;
}
}
void GameSelector::actJoin()
{
return joinGame(false);
}
void GameSelector::actSpectate()
{
return joinGame(true);
}
void GameSelector::customContextMenu(const QPoint &point)
{
const auto &index = gameListView->indexAt(point);
if (!index.isValid()) {
return;
}
QAction joinGame(tr("Join Game"));
connect(&joinGame, &QAction::triggered, this, &GameSelector::actJoin);
QAction spectateGame(tr("Spectate Game"));
connect(&spectateGame, &QAction::triggered, this, &GameSelector::actSpectate);
QAction getGameInfo(tr("Game Information"));
connect(&getGameInfo, &QAction::triggered, this, [=, this]() {
const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt());
const QMap<int, QString> &gameTypes = gameListModel->getGameTypes().value(gameInfo.room_id());
DlgCreateGame dlg(gameInfo, gameTypes, this);
dlg.exec();
});
QMenu menu;
menu.addAction(&joinGame);
menu.addAction(&spectateGame);
menu.addAction(&getGameInfo);
menu.exec(gameListView->mapToGlobal(point));
}
void GameSelector::joinGame(const bool isSpectator)
{
QModelIndex ind = gameListView->currentIndex();
if (!ind.isValid()) {
return;
}
const ServerInfo_Game &game = gameListModel->getGame(ind.data(Qt::UserRole).toInt());
if (tabSupervisor->switchToGameTabIfAlreadyExists(game.game_id())) {
return;
}
bool spectator = isSpectator || game.player_count() == game.max_players();
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
QString password;
if (game.with_password() && !(spectator && !game.spectators_need_password()) && !overrideRestrictions) {
bool ok;
password = getTextWithMax(this, tr("Join game"), tr("Password:"), QLineEdit::Password, QString(), &ok);
if (!ok) {
return;
}
}
Command_JoinGame cmd;
cmd.set_game_id(game.game_id());
cmd.set_password(password.toStdString());
cmd.set_spectator(spectator);
cmd.set_override_restrictions(overrideRestrictions);
cmd.set_join_as_judge((QApplication::keyboardModifiers() & Qt::ShiftModifier) != 0);
TabRoom *r = tabSupervisor->getRoomTabs().value(game.room_id());
if (!r) {
QMessageBox::critical(this, tr("Error"), tr("Please join the respective room first."));
return;
}
PendingCommand *pend = r->prepareRoomCommand(cmd);
connect(pend, &PendingCommand::finished, this, &GameSelector::checkResponse);
r->sendRoomCommand(pend);
disableButtons();
}
void GameSelector::disableButtons()
{
if (createButton)
createButton->setEnabled(false);
joinButton->setEnabled(false);
spectateButton->setEnabled(false);
}
void GameSelector::enableButtons()
{
if (createButton)
createButton->setEnabled(true);
// Enable buttons for the currently selected game
enableButtonsForIndex(gameListView->currentIndex());
}
void GameSelector::enableButtonsForIndex(const QModelIndex &current)
{
if (!current.isValid())
return;
const ServerInfo_Game &game = gameListModel->getGame(current.data(Qt::UserRole).toInt());
bool overrideRestrictions = !tabSupervisor->getAdminLocked();
spectateButton->setEnabled(game.spectators_allowed() || overrideRestrictions);
joinButton->setEnabled(game.player_count() < game.max_players() || overrideRestrictions);
}
void GameSelector::retranslateUi()
{
filterButton->setText(tr("&Filter games"));
clearFilterButton->setText(tr("C&lear filter"));
if (createButton)
createButton->setText(tr("C&reate"));
joinButton->setText(tr("&Join"));
spectateButton->setText(tr("J&oin as spectator"));
updateTitle();
}
void GameSelector::processGameInfo(const ServerInfo_Game &info)
{
gameListModel->updateGameList(info);
updateTitle();
}
void GameSelector::actSelectedGameChanged(const QModelIndex &current, const QModelIndex & /* previous */)
{
enableButtonsForIndex(current);
}
void GameSelector::updateTitle()
{
if (showFilters) {
const int totalGames = gameListModel->rowCount();
const int shownGames = totalGames - gameListProxyModel->getNumFilteredGames();
setTitle(tr("Games shown: %1 / %2").arg(shownGames).arg(totalGames));
} else {
setTitle(tr("Games"));
}
}

View file

@ -0,0 +1,81 @@
/**
* @file game_selector.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef GAMESELECTOR_H
#define GAMESELECTOR_H
#include "game_type_map.h"
#include <QGroupBox>
#include <libcockatrice/protocol/pb/commands.pb.h>
#include <libcockatrice/protocol/pb/event_add_to_list.pb.h>
#include <libcockatrice/protocol/pb/event_remove_from_list.pb.h>
class QTreeView;
class GamesModel;
class GamesProxyModel;
class QPushButton;
class QCheckBox;
class QLabel;
class AbstractClient;
class TabSupervisor;
class TabRoom;
class ServerInfo_Game;
class Response;
class GameSelector : public QGroupBox
{
Q_OBJECT
private slots:
void actSetFilter();
void actClearFilter();
void actCreate();
void actJoin();
void actSpectate();
void customContextMenu(const QPoint &point);
void actSelectedGameChanged(const QModelIndex &current, const QModelIndex &previous);
void checkResponse(const Response &response);
void ignoreListReceived(const QList<ServerInfo_User> &_ignoreList);
void processAddToListEvent(const Event_AddToList &event);
void processRemoveFromListEvent(const Event_RemoveFromList &event);
signals:
void gameJoined(int gameId);
private:
AbstractClient *client;
TabSupervisor *tabSupervisor;
TabRoom *room;
QTreeView *gameListView;
GamesModel *gameListModel;
GamesProxyModel *gameListProxyModel;
QPushButton *filterButton, *clearFilterButton, *createButton, *joinButton, *spectateButton;
const bool showFilters;
GameTypeMap gameTypeMap;
void updateTitle();
void disableButtons();
void enableButtons();
void enableButtonsForIndex(const QModelIndex &current);
void joinGame(const bool isSpectator);
public:
GameSelector(AbstractClient *_client,
TabSupervisor *_tabSupervisor,
TabRoom *_room,
const QMap<int, QString> &_rooms,
const QMap<int, GameTypeMap> &_gameTypes,
const bool restoresettings,
const bool _showfilters,
QWidget *parent = nullptr);
void retranslateUi();
void processGameInfo(const ServerInfo_Game &info);
};
#endif

View file

@ -0,0 +1,8 @@
#ifndef GAMETYPEMAP_H
#define GAMETYPEMAP_H
#include <QMap>
typedef QMap<int, QString> GameTypeMap;
#endif

View file

@ -0,0 +1,581 @@
#include "games_model.h"
#include "../interface/pixel_map_generator.h"
#include "../tabs/tab_account.h"
#include "user/user_list_manager.h"
#include "user/user_list_widget.h"
#include <QDateTime>
#include <QDebug>
#include <QIcon>
#include <QStringList>
#include <QTime>
#include <QTimeZone>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/settings/cache_settings.h>
enum GameListColumn
{
ROOM,
CREATED,
DESCRIPTION,
CREATOR,
GAME_TYPE,
RESTRICTIONS,
PLAYERS,
SPECTATORS
};
const int DEFAULT_MAX_PLAYERS_MIN = 1;
const int DEFAULT_MAX_PLAYERS_MAX = 99;
constexpr QTime DEFAULT_MAX_GAME_AGE = QTime();
const QString GamesModel::getGameCreatedString(const int secs)
{
static const QTime zeroTime{0, 0};
static const int halfHourSecs = zeroTime.secsTo(QTime(1, 0)) / 2;
static const int halfMinSecs = zeroTime.secsTo(QTime(0, 1)) / 2;
static const int wrapSeconds = zeroTime.secsTo(zeroTime.addSecs(-halfHourSecs)); // round up
if (secs >= wrapSeconds) { // QTime wraps after a day
return tr(">1 day");
}
QTime total = zeroTime.addSecs(secs);
QTime totalRounded = total.addSecs(halfMinSecs); // round up
QString form;
int amount;
if (totalRounded.hour()) {
amount = total.addSecs(halfHourSecs).hour(); // round up separately
form = tr("%1%2 hr", "short age in hours", amount);
} else if (total.minute() < 2) { // games are new during their first minute
return tr("new");
} else {
amount = totalRounded.minute();
form = tr("%1%2 min", "short age in minutes", amount);
}
for (int aggregate : {40, 20, 10, 5}) { // floor to values in this list
if (amount >= aggregate) {
return form.arg(">").arg(aggregate);
}
}
return form.arg("").arg(amount);
}
GamesModel::GamesModel(const QMap<int, QString> &_rooms, const QMap<int, GameTypeMap> &_gameTypes, QObject *parent)
: QAbstractTableModel(parent), rooms(_rooms), gameTypes(_gameTypes)
{
}
QVariant GamesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (role == Qt::UserRole)
return index.row();
if (role != Qt::DisplayRole && role != SORT_ROLE && role != Qt::DecorationRole && role != Qt::TextAlignmentRole)
return QVariant();
if ((index.row() >= gameList.size()) || (index.column() >= columnCount()))
return QVariant();
const ServerInfo_Game &gameentry = gameList[index.row()];
switch (index.column()) {
case ROOM:
return rooms.value(gameentry.room_id());
case CREATED: {
switch (role) {
case Qt::DisplayRole: {
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
auto then = QDateTime::fromSecsSinceEpoch(gameentry.start_time(), QTimeZone::UTC);
#else
auto then = QDateTime::fromSecsSinceEpoch(gameentry.start_time(), Qt::UTC);
#endif
int secs = then.secsTo(QDateTime::currentDateTimeUtc());
return getGameCreatedString(secs);
}
case SORT_ROLE:
return QVariant(-static_cast<qint64>(gameentry.start_time()));
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
default:
return QVariant();
}
}
case DESCRIPTION:
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole:
return QString::fromStdString(gameentry.description());
case Qt::TextAlignmentRole:
return Qt::AlignLeft;
default:
return QVariant();
}
case CREATOR: {
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole:
return QString::fromStdString(gameentry.creator_info().name());
case Qt::DecorationRole: {
return UserLevelPixmapGenerator::generateIcon(
13, UserLevelFlags(gameentry.creator_info().user_level()),
gameentry.creator_info().pawn_colors(), false,
QString::fromStdString(gameentry.creator_info().privlevel()));
}
default:
return QVariant();
}
}
case GAME_TYPE:
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole: {
QStringList result;
GameTypeMap gameTypeMap = gameTypes.value(gameentry.room_id());
for (int i = gameentry.game_types_size() - 1; i >= 0; --i)
result.append(gameTypeMap.value(gameentry.game_types(i)));
return result.join(", ");
}
case Qt::TextAlignmentRole:
return Qt::AlignLeft;
default:
return QVariant();
}
case RESTRICTIONS:
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole: {
QStringList result;
if (gameentry.with_password())
result.append(tr("password"));
if (gameentry.only_buddies())
result.append(tr("buddies only"));
if (gameentry.only_registered())
result.append(tr("reg. users only"));
if (gameentry.share_decklists_on_load())
result.append(tr("open decklists"));
return result.join(", ");
}
case Qt::DecorationRole: {
return gameentry.with_password() ? QIcon(LockPixmapGenerator::generatePixmap(13)) : QVariant();
case Qt::TextAlignmentRole:
return Qt::AlignLeft;
default:
return QVariant();
}
}
case PLAYERS:
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole:
return QString("%1/%2").arg(gameentry.player_count()).arg(gameentry.max_players());
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
default:
return QVariant();
}
case SPECTATORS:
switch (role) {
case SORT_ROLE:
case Qt::DisplayRole: {
if (gameentry.spectators_allowed()) {
QString result;
result.append(QString::number(gameentry.spectators_count()));
if (gameentry.spectators_can_chat() && gameentry.spectators_omniscient()) {
result.append(" (")
.append(tr("can chat"))
.append(" & ")
.append(tr("see hands"))
.append(")");
} else if (gameentry.spectators_can_chat()) {
result.append(" (").append(tr("can chat")).append(")");
} else if (gameentry.spectators_omniscient()) {
result.append(" (").append(tr("can see hands")).append(")");
}
return result;
}
return QVariant(tr("not allowed"));
}
case Qt::TextAlignmentRole:
return Qt::AlignLeft;
default:
return QVariant();
}
default:
return QVariant();
}
}
QVariant GamesModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const
{
if ((role != Qt::DisplayRole) && (role != Qt::TextAlignmentRole))
return QVariant();
switch (section) {
case ROOM:
return tr("Room");
case CREATED: {
switch (role) {
case Qt::DisplayRole:
return tr("Age");
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
default:
return QVariant();
}
}
case DESCRIPTION:
return tr("Description");
case CREATOR:
return tr("Creator");
case GAME_TYPE:
return tr("Type");
case RESTRICTIONS:
return tr("Restrictions");
case PLAYERS: {
switch (role) {
case Qt::DisplayRole:
return tr("Players");
case Qt::TextAlignmentRole:
return Qt::AlignCenter;
default:
return QVariant();
}
}
case SPECTATORS:
return tr("Spectators");
default:
return QVariant();
}
}
const ServerInfo_Game &GamesModel::getGame(int row)
{
Q_ASSERT(row < gameList.size());
return gameList[row];
}
void GamesModel::updateGameList(const ServerInfo_Game &game)
{
for (int i = 0; i < gameList.size(); i++) {
if (gameList[i].game_id() == game.game_id()) {
if (game.closed()) {
beginRemoveRows(QModelIndex(), i, i);
gameList.removeAt(i);
endRemoveRows();
} else {
gameList[i].MergeFrom(game);
emit dataChanged(index(i, 0), index(i, NUM_COLS - 1));
}
return;
}
}
beginInsertRows(QModelIndex(), gameList.size(), gameList.size());
gameList.append(game);
endInsertRows();
}
GamesProxyModel::GamesProxyModel(QObject *parent, const UserListProxy *_userListProxy)
: QSortFilterProxyModel(parent), userListProxy(_userListProxy)
{
resetFilterParameters();
setSortRole(GamesModel::SORT_ROLE);
setDynamicSortFilter(true);
}
void GamesProxyModel::setHideBuddiesOnlyGames(bool _showBuddiesOnlyGames)
{
hideBuddiesOnlyGames = _showBuddiesOnlyGames;
invalidateFilter();
}
void GamesProxyModel::setHideIgnoredUserGames(bool _hideIgnoredUserGames)
{
hideIgnoredUserGames = _hideIgnoredUserGames;
invalidateFilter();
}
void GamesProxyModel::setHideFullGames(bool _showFullGames)
{
hideFullGames = _showFullGames;
invalidateFilter();
}
void GamesProxyModel::setHideGamesThatStarted(bool _showGamesThatStarted)
{
hideGamesThatStarted = _showGamesThatStarted;
invalidateFilter();
}
void GamesProxyModel::setHidePasswordProtectedGames(bool _showPasswordProtectedGames)
{
hidePasswordProtectedGames = _showPasswordProtectedGames;
invalidateFilter();
}
void GamesProxyModel::setHideNotBuddyCreatedGames(bool value)
{
hideNotBuddyCreatedGames = value;
invalidateFilter();
}
void GamesProxyModel::setHideOpenDecklistGames(bool _hideOpenDecklistGames)
{
hideOpenDecklistGames = _hideOpenDecklistGames;
invalidateFilter();
}
void GamesProxyModel::setGameNameFilter(const QString &_gameNameFilter)
{
gameNameFilter = _gameNameFilter;
invalidateFilter();
}
void GamesProxyModel::setCreatorNameFilter(const QString &_creatorNameFilter)
{
creatorNameFilter = _creatorNameFilter;
invalidateFilter();
}
void GamesProxyModel::setGameTypeFilter(const QSet<int> &_gameTypeFilter)
{
gameTypeFilter = _gameTypeFilter;
invalidateFilter();
}
void GamesProxyModel::setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax)
{
maxPlayersFilterMin = _maxPlayersFilterMin;
maxPlayersFilterMax = _maxPlayersFilterMax;
invalidateFilter();
}
void GamesProxyModel::setMaxGameAge(const QTime &_maxGameAge)
{
maxGameAge = _maxGameAge;
invalidateFilter();
}
void GamesProxyModel::setShowOnlyIfSpectatorsCanWatch(bool _showOnlyIfSpectatorsCanWatch)
{
showOnlyIfSpectatorsCanWatch = _showOnlyIfSpectatorsCanWatch;
invalidateFilter();
}
void GamesProxyModel::setShowSpectatorPasswordProtected(bool _showSpectatorPasswordProtected)
{
showSpectatorPasswordProtected = _showSpectatorPasswordProtected;
invalidateFilter();
}
void GamesProxyModel::setShowOnlyIfSpectatorsCanChat(bool _showOnlyIfSpectatorsCanChat)
{
showOnlyIfSpectatorsCanChat = _showOnlyIfSpectatorsCanChat;
invalidateFilter();
}
void GamesProxyModel::setShowOnlyIfSpectatorsCanSeeHands(bool _showOnlyIfSpectatorsCanSeeHands)
{
showOnlyIfSpectatorsCanSeeHands = _showOnlyIfSpectatorsCanSeeHands;
invalidateFilter();
}
int GamesProxyModel::getNumFilteredGames() const
{
GamesModel *model = qobject_cast<GamesModel *>(sourceModel());
if (!model)
return 0;
int numFilteredGames = 0;
for (int row = 0; row < model->rowCount(); ++row) {
if (!filterAcceptsRow(row)) {
++numFilteredGames;
}
}
return numFilteredGames;
}
void GamesProxyModel::resetFilterParameters()
{
hideFullGames = false;
hideGamesThatStarted = false;
hidePasswordProtectedGames = false;
hideBuddiesOnlyGames = false;
hideIgnoredUserGames = false;
hideNotBuddyCreatedGames = false;
hideOpenDecklistGames = false;
gameNameFilter = QString();
creatorNameFilter = QString();
gameTypeFilter.clear();
maxPlayersFilterMin = DEFAULT_MAX_PLAYERS_MIN;
maxPlayersFilterMax = DEFAULT_MAX_PLAYERS_MAX;
maxGameAge = DEFAULT_MAX_GAME_AGE;
showOnlyIfSpectatorsCanWatch = false;
showSpectatorPasswordProtected = false;
showOnlyIfSpectatorsCanChat = false;
showOnlyIfSpectatorsCanSeeHands = false;
invalidateFilter();
}
bool GamesProxyModel::areFilterParametersSetToDefaults() const
{
return !hideFullGames && !hideGamesThatStarted && !hidePasswordProtectedGames && !hideBuddiesOnlyGames &&
!hideOpenDecklistGames && !hideIgnoredUserGames && !hideNotBuddyCreatedGames && gameNameFilter.isEmpty() &&
creatorNameFilter.isEmpty() && gameTypeFilter.isEmpty() && maxPlayersFilterMin == DEFAULT_MAX_PLAYERS_MIN &&
maxPlayersFilterMax == DEFAULT_MAX_PLAYERS_MAX && maxGameAge == DEFAULT_MAX_GAME_AGE &&
!showOnlyIfSpectatorsCanWatch && !showSpectatorPasswordProtected && !showOnlyIfSpectatorsCanChat &&
!showOnlyIfSpectatorsCanSeeHands;
}
void GamesProxyModel::loadFilterParameters(const QMap<int, QString> &allGameTypes)
{
GameFiltersSettings &gameFilters = SettingsCache::instance().gameFilters();
hideFullGames = gameFilters.isHideFullGames();
hideGamesThatStarted = gameFilters.isHideGamesThatStarted();
hidePasswordProtectedGames = gameFilters.isHidePasswordProtectedGames();
hideIgnoredUserGames = gameFilters.isHideIgnoredUserGames();
hideBuddiesOnlyGames = gameFilters.isHideBuddiesOnlyGames();
hideNotBuddyCreatedGames = gameFilters.isHideNotBuddyCreatedGames();
hideOpenDecklistGames = gameFilters.isHideOpenDecklistGames();
gameNameFilter = gameFilters.getGameNameFilter();
creatorNameFilter = gameFilters.getCreatorNameFilter();
maxPlayersFilterMin = gameFilters.getMinPlayers();
maxPlayersFilterMax = gameFilters.getMaxPlayers();
maxGameAge = gameFilters.getMaxGameAge();
showOnlyIfSpectatorsCanWatch = gameFilters.isShowOnlyIfSpectatorsCanWatch();
showSpectatorPasswordProtected = gameFilters.isShowSpectatorPasswordProtected();
showOnlyIfSpectatorsCanChat = gameFilters.isShowOnlyIfSpectatorsCanChat();
showOnlyIfSpectatorsCanSeeHands = gameFilters.isShowOnlyIfSpectatorsCanSeeHands();
QMapIterator<int, QString> gameTypesIterator(allGameTypes);
while (gameTypesIterator.hasNext()) {
gameTypesIterator.next();
if (gameFilters.isGameTypeEnabled(gameTypesIterator.value())) {
gameTypeFilter.insert(gameTypesIterator.key());
}
}
invalidateFilter();
}
void GamesProxyModel::saveFilterParameters(const QMap<int, QString> &allGameTypes)
{
GameFiltersSettings &gameFilters = SettingsCache::instance().gameFilters();
gameFilters.setHideBuddiesOnlyGames(hideBuddiesOnlyGames);
gameFilters.setHideFullGames(hideFullGames);
gameFilters.setHideGamesThatStarted(hideGamesThatStarted);
gameFilters.setHidePasswordProtectedGames(hidePasswordProtectedGames);
gameFilters.setHideIgnoredUserGames(hideIgnoredUserGames);
gameFilters.setHideNotBuddyCreatedGames(hideNotBuddyCreatedGames);
gameFilters.setHideOpenDecklistGames(hideOpenDecklistGames);
gameFilters.setGameNameFilter(gameNameFilter);
gameFilters.setCreatorNameFilter(creatorNameFilter);
QMapIterator<int, QString> gameTypeIterator(allGameTypes);
while (gameTypeIterator.hasNext()) {
gameTypeIterator.next();
bool enabled = gameTypeFilter.contains(gameTypeIterator.key());
gameFilters.setGameTypeEnabled(gameTypeIterator.value(), enabled);
}
gameFilters.setMinPlayers(maxPlayersFilterMin);
gameFilters.setMaxPlayers(maxPlayersFilterMax);
gameFilters.setMaxGameAge(maxGameAge);
gameFilters.setShowOnlyIfSpectatorsCanWatch(showOnlyIfSpectatorsCanWatch);
gameFilters.setShowSpectatorPasswordProtected(showSpectatorPasswordProtected);
gameFilters.setShowOnlyIfSpectatorsCanChat(showOnlyIfSpectatorsCanChat);
gameFilters.setShowOnlyIfSpectatorsCanSeeHands(showOnlyIfSpectatorsCanSeeHands);
}
bool GamesProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const
{
return filterAcceptsRow(sourceRow);
}
bool GamesProxyModel::filterAcceptsRow(int sourceRow) const
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, QTimeZone::UTC).date();
#elif (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
static const QDate epochDate = QDateTime::fromSecsSinceEpoch(0, Qt::UTC).date();
#else
static const QDate epochDate = QDateTime::fromTime_t(0, Qt::UTC).date();
#endif
auto *model = qobject_cast<GamesModel *>(sourceModel());
if (!model)
return false;
const ServerInfo_Game &game = model->getGame(sourceRow);
if (hideBuddiesOnlyGames && game.only_buddies()) {
return false;
}
if (hideOpenDecklistGames && game.share_decklists_on_load()) {
return false;
}
if (hideIgnoredUserGames && userListProxy->isUserIgnored(QString::fromStdString(game.creator_info().name()))) {
return false;
}
if (hideNotBuddyCreatedGames && !userListProxy->isUserBuddy(QString::fromStdString(game.creator_info().name()))) {
return false;
}
if (hideFullGames && game.player_count() == game.max_players())
return false;
if (hideGamesThatStarted && game.started())
return false;
if (!userListProxy->isOwnUserRegistered())
if (game.only_registered())
return false;
if (hidePasswordProtectedGames && game.with_password())
return false;
if (!gameNameFilter.isEmpty())
if (!QString::fromStdString(game.description()).contains(gameNameFilter, Qt::CaseInsensitive))
return false;
if (!creatorNameFilter.isEmpty())
if (!QString::fromStdString(game.creator_info().name()).contains(creatorNameFilter, Qt::CaseInsensitive))
return false;
QSet<int> gameTypes;
for (int i = 0; i < game.game_types_size(); ++i)
gameTypes.insert(game.game_types(i));
if (!gameTypeFilter.isEmpty() && gameTypes.intersect(gameTypeFilter).isEmpty())
return false;
if (game.max_players() < maxPlayersFilterMin)
return false;
if (game.max_players() > maxPlayersFilterMax)
return false;
if (maxGameAge.isValid()) {
QDateTime now = QDateTime::currentDateTimeUtc();
qint64 signed_start_time = game.start_time(); // cast to 64 bit value to allow signed value
QDateTime total = now.addSecs(-signed_start_time); // a 32 bit value would wrap at 2038-1-19
// games shouldn't have negative ages but we'll not filter them
// because qtime wraps after a day we consider all games older than a day to be too old
if (total.isValid() && total.date() >= epochDate && (total.date() > epochDate || total.time() > maxGameAge)) {
return false;
}
}
if (showOnlyIfSpectatorsCanWatch) {
if (!game.spectators_allowed())
return false;
if (!showSpectatorPasswordProtected && game.spectators_need_password())
return false;
if (showOnlyIfSpectatorsCanChat && !game.spectators_can_chat())
return false;
if (showOnlyIfSpectatorsCanSeeHands && !game.spectators_omniscient())
return false;
}
return true;
}
void GamesProxyModel::refresh()
{
invalidateFilter();
}

View file

@ -0,0 +1,198 @@
/**
* @file games_model.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef GAMESMODEL_H
#define GAMESMODEL_H
#include "game_type_map.h"
#include <QAbstractTableModel>
#include <QList>
#include <QSet>
#include <QSortFilterProxyModel>
#include <QStringList>
#include <QTime>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
class UserListProxy;
class GamesModel : public QAbstractTableModel
{
Q_OBJECT
private:
QList<ServerInfo_Game> gameList;
QMap<int, QString> rooms;
QMap<int, GameTypeMap> gameTypes;
static const int NUM_COLS = 8;
public:
static const int SORT_ROLE = Qt::UserRole + 1;
GamesModel(const QMap<int, QString> &_rooms, const QMap<int, GameTypeMap> &_gameTypes, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : gameList.size();
}
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override
{
return NUM_COLS;
}
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
static const QString getGameCreatedString(const int secs);
const ServerInfo_Game &getGame(int row);
/**
* Update game list with a (possibly new) game.
*/
void updateGameList(const ServerInfo_Game &game);
int roomColIndex()
{
return 0;
}
int startTimeColIndex()
{
return 1;
}
const QMap<int, GameTypeMap> &getGameTypes()
{
return gameTypes;
}
};
class ServerInfo_User;
class GamesProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
private:
const UserListProxy *userListProxy;
// If adding any additional filters, make sure to update:
// - GamesProxyModel()
// - resetFilterParameters()
// - areFilterParametersSetToDefaults()
// - loadFilterParameters()
// - saveFilterParameters()
// - filterAcceptsRow()
bool hideBuddiesOnlyGames;
bool hideIgnoredUserGames;
bool hideFullGames;
bool hideGamesThatStarted;
bool hidePasswordProtectedGames;
bool hideNotBuddyCreatedGames;
bool hideOpenDecklistGames;
QString gameNameFilter, creatorNameFilter;
QSet<int> gameTypeFilter;
quint32 maxPlayersFilterMin, maxPlayersFilterMax;
QTime maxGameAge;
bool showOnlyIfSpectatorsCanWatch, showSpectatorPasswordProtected, showOnlyIfSpectatorsCanChat,
showOnlyIfSpectatorsCanSeeHands;
public:
explicit GamesProxyModel(QObject *parent = nullptr, const UserListProxy *_userListProxy = nullptr);
bool getHideBuddiesOnlyGames() const
{
return hideBuddiesOnlyGames;
}
void setHideBuddiesOnlyGames(bool _showBuddiesOnlyGames);
bool getHideIgnoredUserGames() const
{
return hideIgnoredUserGames;
}
void setHideIgnoredUserGames(bool _hideIgnoredUserGames);
bool getHideFullGames() const
{
return hideFullGames;
}
void setHideFullGames(bool _showFullGames);
bool getHideGamesThatStarted() const
{
return hideGamesThatStarted;
}
void setHideGamesThatStarted(bool _showGamesThatStarted);
bool getHidePasswordProtectedGames() const
{
return hidePasswordProtectedGames;
}
void setHidePasswordProtectedGames(bool _showPasswordProtectedGames);
bool getHideNotBuddyCreatedGames() const
{
return hideNotBuddyCreatedGames;
}
void setHideNotBuddyCreatedGames(bool value);
bool getHideOpenDecklistGames() const
{
return hideOpenDecklistGames;
}
void setHideOpenDecklistGames(bool _hideOpenDecklistGames);
QString getGameNameFilter() const
{
return gameNameFilter;
}
void setGameNameFilter(const QString &_gameNameFilter);
QString getCreatorNameFilter() const
{
return creatorNameFilter;
}
void setCreatorNameFilter(const QString &_creatorNameFilter);
QSet<int> getGameTypeFilter() const
{
return gameTypeFilter;
}
void setGameTypeFilter(const QSet<int> &_gameTypeFilter);
int getMaxPlayersFilterMin() const
{
return maxPlayersFilterMin;
}
int getMaxPlayersFilterMax() const
{
return maxPlayersFilterMax;
}
void setMaxPlayersFilter(int _maxPlayersFilterMin, int _maxPlayersFilterMax);
const QTime &getMaxGameAge() const
{
return maxGameAge;
}
void setMaxGameAge(const QTime &_maxGameAge);
bool getShowOnlyIfSpectatorsCanWatch() const
{
return showOnlyIfSpectatorsCanWatch;
}
void setShowOnlyIfSpectatorsCanWatch(bool _showOnlyIfSpectatorsCanWatch);
bool getShowSpectatorPasswordProtected() const
{
return showSpectatorPasswordProtected;
}
void setShowSpectatorPasswordProtected(bool _showSpectatorPasswordProtected);
bool getShowOnlyIfSpectatorsCanChat() const
{
return showOnlyIfSpectatorsCanChat;
}
void setShowOnlyIfSpectatorsCanChat(bool _showOnlyIfSpectatorsCanChat);
bool getShowOnlyIfSpectatorsCanSeeHands() const
{
return showOnlyIfSpectatorsCanSeeHands;
}
void setShowOnlyIfSpectatorsCanSeeHands(bool _showOnlyIfSpectatorsCanSeeHands);
int getNumFilteredGames() const;
void resetFilterParameters();
bool areFilterParametersSetToDefaults() const;
void loadFilterParameters(const QMap<int, QString> &allGameTypes);
void saveFilterParameters(const QMap<int, QString> &allGameTypes);
void refresh();
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
bool filterAcceptsRow(int sourceRow) const;
};
#endif

View file

@ -0,0 +1,109 @@
#include "handle_public_servers.h"
#include <QJsonDocument>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
#include <libcockatrice/settings/cache_settings.h>
#define PUBLIC_SERVERS_JSON "https://cockatrice.github.io/public-servers.json"
HandlePublicServers::HandlePublicServers(QObject *parent)
: QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr)
{
}
void HandlePublicServers::downloadPublicServers()
{
QUrl url(QString(PUBLIC_SERVERS_JSON));
reply = nam->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, &HandlePublicServers::actFinishParsingDownloadedData);
}
void HandlePublicServers::actFinishParsingDownloadedData()
{
reply = dynamic_cast<QNetworkReply *>(sender());
QNetworkReply::NetworkError errorCode = reply->error();
if (errorCode == QNetworkReply::NoError) {
// Get current saved hosts
UserConnection_Information uci;
savedHostList = uci.getServerInfo();
// Downloaded data from GitHub
QJsonParseError parseError{};
QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
if (parseError.error == QJsonParseError::NoError) {
QVariantMap jsonMap = jsonResponse.toVariant().toMap();
updateServerINISettings(jsonMap);
} else {
qDebug() << "[PUBLIC SERVER HANDLER]"
<< "JSON Parsing Error:" << parseError.errorString();
emit sigPublicServersDownloadedUnsuccessfully(errorCode);
}
} else {
qDebug() << "[PUBLIC SERVER HANDLER]"
<< "Error Downloading Public Servers" << errorCode;
emit sigPublicServersDownloadedUnsuccessfully(errorCode);
}
reply->deleteLater(); // After an emit() occurs, this object will be deleted
}
void HandlePublicServers::updateServerINISettings(QMap<QString, QVariant> jsonMap)
{
// Servers available
auto publicServersJSONList = jsonMap["servers"].toList();
for (const auto &server : publicServersJSONList) {
// Data inside one server at a time
// server: [{ ... }, ..., { ... }]
const auto serverMap = server.toMap();
QString serverAddress = serverMap["host"].toString();
if (serverMap["isInactive"].toBool()) {
publicServersToRemove.append(serverAddress);
continue;
}
QString serverName = serverMap["name"].toString();
QString serverPort = serverMap["port"].toString();
QString serverSite = serverMap["site"].toString();
if (serverMap.contains("websocketPort")) {
serverPort = serverMap["websocketPort"].toString();
}
bool serverFound = false;
for (const auto &iter : savedHostList) {
// If the URL/IP matches
if (iter.second.getServer() == serverAddress) {
serverFound = true;
break;
}
}
if (serverFound) {
SettingsCache::instance().servers().updateExistingServerWithoutLoss(serverName, serverAddress, serverPort,
serverSite);
} else {
SettingsCache::instance().servers().addNewServer(serverName, serverAddress, serverPort, "", "", false,
serverSite);
}
}
// If a server was removed from the public list,
// we will delete it from the local system.
// Will not delete "unofficial" servers
for (const auto &pair : savedHostList) {
QString serverAddr = pair.first;
if (publicServersToRemove.indexOf(serverAddr) != -1) {
SettingsCache::instance().servers().removeServer(serverAddr);
}
}
emit sigPublicServersDownloadedSuccessfully();
}

View file

@ -0,0 +1,46 @@
/**
* @file handle_public_servers.h
* @ingroup Server
* @brief TODO: Document this.
*/
#ifndef COCKATRICE_HANDLE_PUBLIC_SERVERS_H
#define COCKATRICE_HANDLE_PUBLIC_SERVERS_H
#include "user/user_info_connection.h"
class QNetworkReply;
class QNetworkAccessManager;
/**
* This class is used to update the servers.ini file and ensure
* the list of public servers has up-to-date information.
* Servers that are added manually by users are not modified.
*/
class HandlePublicServers : public QObject
{
Q_OBJECT
signals:
void sigPublicServersDownloadedSuccessfully();
void sigPublicServersDownloadedUnsuccessfully(int);
public:
explicit HandlePublicServers(QObject *parent = nullptr);
~HandlePublicServers() override = default;
public slots:
void downloadPublicServers();
private slots:
void actFinishParsingDownloadedData();
private:
void updateServerINISettings(QMap<QString, QVariant>);
QStringList publicServersToRemove;
QMap<QString, std::pair<QString, UserConnection_Information>> savedHostList;
QNetworkAccessManager *nam;
QNetworkReply *reply;
};
#endif // COCKATRICE_HANDLE_PUBLIC_SERVERS_H

View file

@ -0,0 +1,368 @@
#include "remote_decklist_tree_widget.h"
#include <QFileIconProvider>
#include <QHeaderView>
#include <QSortFilterProxyModel>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
#include <libcockatrice/protocol/pending_command.h>
RemoteDeckList_TreeModel::DirectoryNode::DirectoryNode(const QString &_name,
RemoteDeckList_TreeModel::DirectoryNode *_parent)
: RemoteDeckList_TreeModel::Node(_name, _parent)
{
}
RemoteDeckList_TreeModel::DirectoryNode::~DirectoryNode()
{
clearTree();
}
void RemoteDeckList_TreeModel::DirectoryNode::clearTree()
{
for (int i = 0; i < size(); ++i)
delete at(i);
clear();
}
QString RemoteDeckList_TreeModel::DirectoryNode::getPath() const
{
if (parent) {
QString parentPath = parent->getPath();
if (parentPath.isEmpty())
return name;
else
return parentPath + "/" + name;
} else
return name;
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeModel::DirectoryNode::getNodeByPath(QStringList path)
{
QString pathItem;
if (parent) {
if (path.isEmpty())
return this;
pathItem = path.takeFirst();
if (pathItem.isEmpty() && name.isEmpty())
return this;
}
for (int i = 0; i < size(); ++i) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(at(i));
if (!node)
continue;
if (node->getName() == pathItem)
return node->getNodeByPath(path);
}
return 0;
}
RemoteDeckList_TreeModel::FileNode *RemoteDeckList_TreeModel::DirectoryNode::getNodeById(int id) const
{
for (int i = 0; i < size(); ++i) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(at(i));
if (node) {
FileNode *result = node->getNodeById(id);
if (result)
return result;
} else {
FileNode *file = dynamic_cast<FileNode *>(at(i));
if (file->getId() == id)
return file;
}
}
return 0;
}
RemoteDeckList_TreeModel::RemoteDeckList_TreeModel(AbstractClient *_client, QObject *parent)
: QAbstractItemModel(parent), client(_client)
{
QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File);
root = new DirectoryNode;
}
RemoteDeckList_TreeModel::~RemoteDeckList_TreeModel()
{
delete root;
}
int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const
{
DirectoryNode *node = getNode<DirectoryNode *>(parent);
if (node)
return node->size();
else
return 0;
}
int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const
{
return 3;
}
QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.column() >= 3)
return QVariant();
Node *temp = static_cast<Node *>(index.internalPointer());
FileNode *file = dynamic_cast<FileNode *>(temp);
if (!file) {
DirectoryNode *node = dynamic_cast<DirectoryNode *>(temp);
switch (role) {
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return node->getName();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? dirIcon : QVariant();
default:
return QVariant();
}
} else {
switch (role) {
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return file->getName();
case 1:
return file->getId();
case 2:
return file->getUploadTime();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? fileIcon : QVariant();
case Qt::TextAlignmentRole:
return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft;
default:
return QVariant();
}
}
}
QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
return QVariant();
switch (role) {
case Qt::TextAlignmentRole:
return section == 1 ? Qt::AlignRight : Qt::AlignLeft;
case Qt::DisplayRole: {
switch (section) {
case 0:
return tr("Name");
case 1:
return tr("ID");
case 2:
return tr("Upload time");
default:
return QVariant();
}
}
default:
return QVariant();
}
}
QModelIndex RemoteDeckList_TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
DirectoryNode *parentNode = getNode<DirectoryNode *>(parent);
if (row >= parentNode->size())
return QModelIndex();
return createIndex(row, column, parentNode->at(row));
}
QModelIndex RemoteDeckList_TreeModel::parent(const QModelIndex &ind) const
{
if (!ind.isValid())
return QModelIndex();
return nodeToIndex(static_cast<Node *>(ind.internalPointer())->getParent());
}
Qt::ItemFlags RemoteDeckList_TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QModelIndex RemoteDeckList_TreeModel::nodeToIndex(Node *node) const
{
if (node == nullptr || node == root)
return QModelIndex();
return createIndex(node->getParent()->indexOf(node), 0, node);
}
void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent)
{
const ServerInfo_DeckStorage_File &fileInfo = file.file();
QDateTime time;
time.setSecsSinceEpoch(fileInfo.creation_time());
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent));
endInsertRows();
}
void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent)
{
DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent);
const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder();
const int folderItemsSize = folderInfo.items_size();
for (int i = 0; i < folderItemsSize; ++i) {
const ServerInfo_DeckStorage_TreeItem &subItem = folderInfo.items(i);
if (subItem.has_folder())
addFolderToTree(subItem, newItem);
else
addFileToTree(subItem, newItem);
}
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeModel::addNamedFolderToTree(const QString &name,
DirectoryNode *parent)
{
DirectoryNode *newItem = new DirectoryNode(name, parent);
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
parent->append(newItem);
endInsertRows();
return newItem;
}
void RemoteDeckList_TreeModel::removeNode(RemoteDeckList_TreeModel::Node *node)
{
int ind = node->getParent()->indexOf(node);
beginRemoveRows(nodeToIndex(node->getParent()), ind, ind);
node->getParent()->removeAt(ind);
endRemoveRows();
delete node;
}
void RemoteDeckList_TreeModel::refreshTree()
{
PendingCommand *pend = client->prepareSessionCommand(Command_DeckList());
connect(pend, &PendingCommand::finished, this, &RemoteDeckList_TreeModel::deckListFinished);
client->sendCommand(pend);
}
void RemoteDeckList_TreeModel::clearTree()
{
beginResetModel();
root->clearTree();
endResetModel();
}
void RemoteDeckList_TreeModel::deckListFinished(const Response &r)
{
const Response_DeckList &resp = r.GetExtension(Response_DeckList::ext);
clearTree();
ServerInfo_DeckStorage_TreeItem tempRoot;
tempRoot.set_id(0);
tempRoot.mutable_folder()->CopyFrom(resp.root());
addFolderToTree(tempRoot, root);
emit treeRefreshed();
}
RemoteDeckList_TreeWidget::RemoteDeckList_TreeWidget(AbstractClient *_client, QWidget *parent) : QTreeView(parent)
{
treeModel = new RemoteDeckList_TreeModel(_client, this);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(treeModel);
proxyModel->setDynamicSortFilter(true);
proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(proxyModel);
connect(treeModel, &RemoteDeckList_TreeModel::treeRefreshed, this, &RemoteDeckList_TreeWidget::expandAll);
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
setUniformRowHeights(true);
setSelectionMode(QAbstractItemView::ExtendedSelection);
setSortingEnabled(true);
proxyModel->sort(0, Qt::AscendingOrder);
header()->setSortIndicator(0, Qt::AscendingOrder);
}
RemoteDeckList_TreeModel::Node *RemoteDeckList_TreeWidget::getNode(const QModelIndex &ind) const
{
return treeModel->getNode<RemoteDeckList_TreeModel::Node *>(proxyModel->mapToSource(ind));
}
RemoteDeckList_TreeModel::Node *RemoteDeckList_TreeWidget::getCurrentItem() const
{
return getNode(selectionModel()->currentIndex());
}
QList<RemoteDeckList_TreeModel::Node *> RemoteDeckList_TreeWidget::getCurrentSelection() const
{
auto list = QList<RemoteDeckList_TreeModel::Node *>();
for (const auto &row : selectionModel()->selectedRows()) {
list << getNode(row);
}
return list;
}
RemoteDeckList_TreeModel::DirectoryNode *RemoteDeckList_TreeWidget::getNodeByPath(const QString &path) const
{
return treeModel->getRoot()->getNodeByPath(path.split("/"));
}
RemoteDeckList_TreeModel::FileNode *RemoteDeckList_TreeWidget::getNodeById(int id) const
{
return treeModel->getRoot()->getNodeById(id);
}
void RemoteDeckList_TreeWidget::addFileToTree(const ServerInfo_DeckStorage_TreeItem &file,
RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addFileToTree(file, parent);
}
void RemoteDeckList_TreeWidget::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder,
RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addFolderToTree(folder, parent);
}
void RemoteDeckList_TreeWidget::addFolderToTree(const QString &name, RemoteDeckList_TreeModel::DirectoryNode *parent)
{
treeModel->addNamedFolderToTree(name, parent);
}
void RemoteDeckList_TreeWidget::removeNode(RemoteDeckList_TreeModel::Node *node)
{
treeModel->removeNode(node);
}
void RemoteDeckList_TreeWidget::refreshTree()
{
treeModel->refreshTree();
}
void RemoteDeckList_TreeWidget::clearTree()
{
treeModel->clearTree();
}

View file

@ -0,0 +1,141 @@
/**
* @file remote_decklist_tree_widget.h
* @ingroup NetworkingWidgets
* @ingroup DeckStorageWidgets
* @brief TODO: Document this.
*/
#ifndef REMOTEDECKLIST_TREEWIDGET_H
#define REMOTEDECKLIST_TREEWIDGET_H
#include <QAbstractItemModel>
#include <QDateTime>
#include <QTreeView>
class Response;
class AbstractClient;
class QSortFilterProxyModel;
class ServerInfo_DeckStorage_TreeItem;
class RemoteDeckList_TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
class DirectoryNode;
class FileNode;
class Node
{
protected:
DirectoryNode *parent;
QString name;
public:
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name)
{
}
virtual ~Node() = default;
DirectoryNode *getParent() const
{
return parent;
}
QString getName() const
{
return name;
}
};
class DirectoryNode : public Node, public QList<Node *>
{
public:
explicit DirectoryNode(const QString &_name = QString(), DirectoryNode *_parent = nullptr);
~DirectoryNode() override;
void clearTree();
QString getPath() const;
DirectoryNode *getNodeByPath(QStringList path);
FileNode *getNodeById(int id) const;
};
class FileNode : public Node
{
private:
int id;
QDateTime uploadTime;
public:
FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr)
: Node(_name, _parent), id(_id), uploadTime(_uploadTime)
{
}
int getId() const
{
return id;
}
QDateTime getUploadTime() const
{
return uploadTime;
}
};
template <typename T> T getNode(const QModelIndex &index) const
{
if (!index.isValid())
return dynamic_cast<T>(root);
return dynamic_cast<T>(static_cast<Node *>(index.internalPointer()));
}
private:
AbstractClient *client;
DirectoryNode *root;
QIcon fileIcon, dirIcon;
QModelIndex nodeToIndex(Node *node) const;
signals:
void treeRefreshed();
private slots:
void deckListFinished(const Response &r);
public:
explicit RemoteDeckList_TreeModel(AbstractClient *_client, QObject *parent = nullptr);
~RemoteDeckList_TreeModel() override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
DirectoryNode *getRoot() const
{
return root;
}
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent);
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent);
DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent);
void removeNode(Node *node);
void refreshTree();
void clearTree();
};
class RemoteDeckList_TreeWidget : public QTreeView
{
private:
RemoteDeckList_TreeModel *treeModel;
QSortFilterProxyModel *proxyModel;
public:
explicit RemoteDeckList_TreeWidget(AbstractClient *_client, QWidget *parent = nullptr);
RemoteDeckList_TreeModel::Node *getNode(const QModelIndex &ind) const;
RemoteDeckList_TreeModel::Node *getCurrentItem() const;
QList<RemoteDeckList_TreeModel::Node *> getCurrentSelection() const;
RemoteDeckList_TreeModel::DirectoryNode *getNodeByPath(const QString &path) const;
RemoteDeckList_TreeModel::FileNode *getNodeById(int id) const;
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, RemoteDeckList_TreeModel::DirectoryNode *parent);
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder,
RemoteDeckList_TreeModel::DirectoryNode *parent);
void addFolderToTree(const QString &name, RemoteDeckList_TreeModel::DirectoryNode *parent);
void removeNode(RemoteDeckList_TreeModel::Node *node);
void refreshTree();
void clearTree();
};
#endif

View file

@ -0,0 +1,383 @@
#include "remote_replay_list_tree_widget.h"
#include <QFileIconProvider>
#include <QHeaderView>
#include <QSortFilterProxyModel>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_replay_list.pb.h>
#include <libcockatrice/protocol/pb/response_replay_list.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_replay.pb.h>
#include <libcockatrice/protocol/pending_command.h>
const int RemoteReplayList_TreeModel::numberOfColumns = 6;
RemoteReplayList_TreeModel::MatchNode::MatchNode(const ServerInfo_ReplayMatch &_matchInfo)
: RemoteReplayList_TreeModel::Node(QString::fromStdString(_matchInfo.game_name())), matchInfo(_matchInfo)
{
for (int i = 0; i < matchInfo.replay_list_size(); ++i)
append(new ReplayNode(matchInfo.replay_list(i), this));
}
RemoteReplayList_TreeModel::MatchNode::~MatchNode()
{
for (int i = 0; i < size(); ++i)
delete at(i);
}
void RemoteReplayList_TreeModel::MatchNode::updateMatchInfo(const ServerInfo_ReplayMatch &_matchInfo)
{
matchInfo.MergeFrom(_matchInfo);
}
RemoteReplayList_TreeModel::RemoteReplayList_TreeModel(AbstractClient *_client, QObject *parent)
: QAbstractItemModel(parent), client(_client)
{
QFileIconProvider fip;
dirIcon = fip.icon(QFileIconProvider::Folder);
fileIcon = fip.icon(QFileIconProvider::File);
lockIcon = QPixmap("theme:icons/lock");
}
RemoteReplayList_TreeModel::~RemoteReplayList_TreeModel()
{
clearAll();
}
int RemoteReplayList_TreeModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid())
return replayMatches.size();
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(parent.internalPointer()));
if (matchNode)
return matchNode->size();
else
return 0;
}
QVariant RemoteReplayList_TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.column() >= numberOfColumns)
return QVariant();
ReplayNode *replayNode = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (replayNode) {
const ServerInfo_Replay &replayInfo = replayNode->getReplayInfo();
switch (role) {
case Qt::TextAlignmentRole:
return index.column() == 0 ? Qt::AlignRight : Qt::AlignLeft;
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return replayInfo.replay_id();
case 1:
return QString::fromStdString(replayInfo.replay_name());
case 5:
return replayInfo.duration();
default:
return QVariant();
}
}
case Qt::DecorationRole:
return index.column() == 0 ? fileIcon : QVariant();
}
} else {
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(index.internalPointer()));
const ServerInfo_ReplayMatch &matchInfo = matchNode->getMatchInfo();
switch (role) {
case Qt::TextAlignmentRole:
switch (index.column()) {
case 0:
case 5:
return Qt::AlignRight;
default:
return Qt::AlignLeft;
}
case Qt::DisplayRole: {
switch (index.column()) {
case 0:
return matchInfo.game_id();
case 1:
return QString::fromStdString(matchInfo.game_name());
case 2: {
QStringList playerList;
for (int i = 0; i < matchInfo.player_names_size(); ++i)
playerList.append(QString::fromStdString(matchInfo.player_names(i)));
return playerList.join(", ");
}
case 4:
return QDateTime::fromSecsSinceEpoch(matchInfo.time_started());
case 5:
return matchInfo.length();
default:
return QVariant();
}
}
case Qt::DecorationRole:
switch (index.column()) {
case 0:
return dirIcon;
case 3:
return matchInfo.do_not_hide() ? lockIcon : QVariant();
default:
return QVariant();
}
}
}
return QVariant();
}
QVariant RemoteReplayList_TreeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal)
return QVariant();
switch (role) {
case Qt::TextAlignmentRole:
switch (section) {
case 0:
case 5:
return Qt::AlignRight;
default:
return Qt::AlignLeft;
}
case Qt::DisplayRole: {
switch (section) {
case 0:
return tr("ID");
case 1:
return tr("Name");
case 2:
return tr("Players");
case 3:
return tr("Keep");
case 4:
return tr("Time started");
case 5:
return tr("Duration (sec)");
default:
return QVariant();
}
}
default:
return QVariant();
}
}
QModelIndex RemoteReplayList_TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
MatchNode *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(parent.internalPointer()));
if (matchNode) {
if (row >= matchNode->size())
return QModelIndex();
return createIndex(row, column, (void *)matchNode->at(row));
} else {
if (row >= replayMatches.size())
return QModelIndex();
return createIndex(row, column, (void *)replayMatches[row]);
}
}
QModelIndex RemoteReplayList_TreeModel::parent(const QModelIndex &ind) const
{
MatchNode const *matchNode = dynamic_cast<MatchNode *>(static_cast<Node *>(ind.internalPointer()));
if (matchNode)
return QModelIndex();
else {
ReplayNode *replayNode = dynamic_cast<ReplayNode *>(static_cast<Node *>(ind.internalPointer()));
return createIndex(replayNode->getParent()->indexOf(replayNode), 0, replayNode->getParent());
}
}
Qt::ItemFlags RemoteReplayList_TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
ServerInfo_Replay const *RemoteReplayList_TreeModel::getReplay(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
ReplayNode *node = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (!node)
return 0;
return &node->getReplayInfo();
}
ServerInfo_ReplayMatch const *RemoteReplayList_TreeModel::getReplayMatch(const QModelIndex &index) const
{
if (!index.isValid())
return nullptr;
auto *node = dynamic_cast<MatchNode *>(static_cast<Node *>(index.internalPointer()));
if (!node)
return nullptr;
return &node->getMatchInfo();
}
ServerInfo_ReplayMatch const *RemoteReplayList_TreeModel::getEnclosingReplayMatch(const QModelIndex &index) const
{
if (!index.isValid())
return nullptr;
auto *node = dynamic_cast<MatchNode *>(static_cast<Node *>(index.internalPointer()));
if (!node) {
auto *_node = dynamic_cast<ReplayNode *>(static_cast<Node *>(index.internalPointer()));
if (!_node)
return nullptr;
return &_node->getParent()->getMatchInfo();
}
return &node->getMatchInfo();
}
/**
* Deletes all items in the model
*/
void RemoteReplayList_TreeModel::clearAll()
{
for (int i = 0; i < replayMatches.size(); ++i)
delete replayMatches[i];
replayMatches.clear();
}
void RemoteReplayList_TreeModel::refreshTree()
{
PendingCommand *pend = client->prepareSessionCommand(Command_ReplayList());
connect(pend, &PendingCommand::finished, this, &RemoteReplayList_TreeModel::replayListFinished);
client->sendCommand(pend);
}
void RemoteReplayList_TreeModel::clearTree()
{
beginResetModel();
clearAll();
endResetModel();
}
void RemoteReplayList_TreeModel::addMatchInfo(const ServerInfo_ReplayMatch &matchInfo)
{
beginInsertRows(QModelIndex(), replayMatches.size(), replayMatches.size());
replayMatches.append(new MatchNode(matchInfo));
endInsertRows();
emit treeRefreshed();
}
void RemoteReplayList_TreeModel::updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo)
{
for (int i = 0; i < replayMatches.size(); ++i)
if (replayMatches[i]->getMatchInfo().game_id() == gameId) {
replayMatches[i]->updateMatchInfo(matchInfo);
emit dataChanged(createIndex(i, 0, (void *)replayMatches[i]),
createIndex(i, numberOfColumns - 1, (void *)replayMatches[i]));
break;
}
}
void RemoteReplayList_TreeModel::removeMatchInfo(int gameId)
{
for (int i = 0; i < replayMatches.size(); ++i)
if (replayMatches[i]->getMatchInfo().game_id() == gameId) {
beginRemoveRows(QModelIndex(), i, i);
replayMatches.removeAt(i);
endRemoveRows();
break;
}
}
void RemoteReplayList_TreeModel::replayListFinished(const Response &r)
{
const Response_ReplayList &resp = r.GetExtension(Response_ReplayList::ext);
beginResetModel();
clearAll();
for (int i = 0; i < resp.match_list_size(); ++i)
replayMatches.append(new MatchNode(resp.match_list(i)));
endResetModel();
emit treeRefreshed();
}
RemoteReplayList_TreeWidget::RemoteReplayList_TreeWidget(AbstractClient *_client, QWidget *parent) : QTreeView(parent)
{
treeModel = new RemoteReplayList_TreeModel(_client, this);
proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(treeModel);
proxyModel->setDynamicSortFilter(true);
proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(proxyModel);
header()->setSectionResizeMode(QHeaderView::ResizeToContents);
header()->setStretchLastSection(false);
setUniformRowHeights(true);
setSortingEnabled(true);
proxyModel->sort(0, Qt::AscendingOrder);
header()->setSortIndicator(0, Qt::AscendingOrder);
setSelectionMode(QAbstractItemView::ExtendedSelection);
}
/**
* Gets the replay at the given index
* @return The replay. Returns nullptr if there is no replay at the index.
*/
ServerInfo_Replay const *RemoteReplayList_TreeWidget::getReplay(const QModelIndex &ind) const
{
return treeModel->getReplay(proxyModel->mapToSource(ind));
}
/**
* Gets the replay match at the given index
* @return The replay match. Returns nullptr if there is no replay match at the index.
*/
ServerInfo_ReplayMatch const *RemoteReplayList_TreeWidget::getReplayMatch(const QModelIndex &ind) const
{
return treeModel->getReplayMatch(proxyModel->mapToSource(ind));
}
/**
* Gets all currently selected replays.
* Any selection that isn't a replay file (e.g. a folder) will appear as a nullptr in the list.
* Make sure to check the list for nullptr before using it.
*
* @return A List of pointers to the selected replays, as well as nullptr for any selection that isn't a replay.
*/
QList<ServerInfo_Replay const *> RemoteReplayList_TreeWidget::getSelectedReplays() const
{
const auto selection = selectionModel()->selectedRows();
auto replays = QList<ServerInfo_Replay const *>();
for (const auto &row : selection) {
replays << treeModel->getReplay(proxyModel->mapToSource(row));
}
return replays;
}
/**
* Gets all currently selected replayMatches.
* If a non-folder node is selected, it will return the parent folder of that node.
*
* @return A Set of pointers to the selected replayMatches.
*/
QSet<ServerInfo_ReplayMatch const *> RemoteReplayList_TreeWidget::getSelectedReplayMatches() const
{
const auto selection = selectionModel()->selectedRows();
auto replayMatches = QSet<ServerInfo_ReplayMatch const *>();
for (const auto &row : selection) {
if (const auto replayMatch = treeModel->getEnclosingReplayMatch(proxyModel->mapToSource(row))) {
replayMatches << replayMatch;
}
}
return replayMatches;
}

View file

@ -0,0 +1,148 @@
/**
* @file remote_replay_list_tree_widget.h
* @ingroup DeckStorageWidgets
* @ingroup Replays
* @ingroup NetworkingWidgets
* @brief TODO: Document this.
*/
#ifndef REMOTEREPLAYLIST_TREEWIDGET_H
#define REMOTEREPLAYLIST_TREEWIDGET_H
#include <QAbstractItemModel>
#include <QDateTime>
#include <QTreeView>
#include <libcockatrice/protocol/pb/serverinfo_replay.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_replay_match.pb.h>
class Response;
class AbstractClient;
class QSortFilterProxyModel;
class RemoteReplayList_TreeModel : public QAbstractItemModel
{
Q_OBJECT
private:
class MatchNode;
class ReplayNode;
class Node
{
protected:
QString name;
public:
explicit Node(const QString &_name) : name(_name)
{
}
virtual ~Node() = default;
QString getName() const
{
return name;
}
};
class MatchNode : public Node, public QList<ReplayNode *>
{
private:
ServerInfo_ReplayMatch matchInfo;
public:
explicit MatchNode(const ServerInfo_ReplayMatch &_matchInfo);
~MatchNode() override;
void clearTree();
const ServerInfo_ReplayMatch &getMatchInfo()
{
return matchInfo;
}
void updateMatchInfo(const ServerInfo_ReplayMatch &_matchInfo);
};
class ReplayNode : public Node
{
private:
MatchNode *parent;
ServerInfo_Replay replayInfo;
public:
ReplayNode(const ServerInfo_Replay &_replayInfo, MatchNode *_parent)
: Node(QString::fromStdString(_replayInfo.replay_name())), parent(_parent), replayInfo(_replayInfo)
{
}
MatchNode *getParent() const
{
return parent;
}
const ServerInfo_Replay &getReplayInfo()
{
return replayInfo;
}
};
AbstractClient *client;
QList<MatchNode *> replayMatches;
QIcon dirIcon, fileIcon, lockIcon;
void clearAll();
static const int numberOfColumns;
signals:
void treeRefreshed();
private slots:
void replayListFinished(const Response &r);
public:
explicit RemoteReplayList_TreeModel(AbstractClient *_client, QObject *parent = nullptr);
~RemoteReplayList_TreeModel() override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override
{
return numberOfColumns;
}
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void clearTree();
void refreshTree();
ServerInfo_Replay const *getReplay(const QModelIndex &index) const;
ServerInfo_ReplayMatch const *getReplayMatch(const QModelIndex &index) const;
ServerInfo_ReplayMatch const *getEnclosingReplayMatch(const QModelIndex &index) const;
void addMatchInfo(const ServerInfo_ReplayMatch &matchInfo);
void updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo);
void removeMatchInfo(int gameId);
};
class RemoteReplayList_TreeWidget : public QTreeView
{
private:
RemoteReplayList_TreeModel *treeModel;
QSortFilterProxyModel *proxyModel;
public:
explicit RemoteReplayList_TreeWidget(AbstractClient *_client, QWidget *parent = nullptr);
ServerInfo_Replay const *getReplay(const QModelIndex &ind) const;
ServerInfo_ReplayMatch const *getReplayMatch(const QModelIndex &ind) const;
QList<ServerInfo_Replay const *> getSelectedReplays() const;
QSet<ServerInfo_ReplayMatch const *> getSelectedReplayMatches() const;
void clearTree()
{
treeModel->clearTree();
}
void refreshTree()
{
treeModel->refreshTree();
}
void addMatchInfo(const ServerInfo_ReplayMatch &matchInfo)
{
treeModel->addMatchInfo(matchInfo);
}
void updateMatchInfo(int gameId, const ServerInfo_ReplayMatch &matchInfo)
{
treeModel->updateMatchInfo(gameId, matchInfo);
}
void removeMatchInfo(int gameId)
{
treeModel->removeMatchInfo(gameId);
}
};
#endif

View file

@ -0,0 +1,536 @@
#include "user_context_menu.h"
#include "../../tabs/tab_account.h"
#include "../../tabs/tab_game.h"
#include "../../tabs/tab_supervisor.h"
#include "../chat_view/chat_view.h"
#include "../game_selector.h"
#include "user_info_box.h"
#include "user_list_manager.h"
#include "user_list_proxy.h"
#include "user_list_widget.h"
#include <QAction>
#include <QMenu>
#include <QMessageBox>
#include <QSignalMapper>
#include <QtGui>
#include <QtWidgets>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_kick_from_game.pb.h>
#include <libcockatrice/protocol/pb/commands.pb.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_ban_history.pb.h>
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pb/response_warn_history.pb.h>
#include <libcockatrice/protocol/pb/response_warn_list.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent, AbstractGame *_game)
: QObject(parent), client(_tabSupervisor->getClient()), tabSupervisor(_tabSupervisor),
userListProxy(_tabSupervisor->getUserListManager()), game(_game)
{
aUserName = new QAction(QString(), this);
aUserName->setEnabled(false);
aDetails = new QAction(QString(), this);
aChat = new QAction(QString(), this);
aShowGames = new QAction(QString(), this);
aAddToBuddyList = new QAction(QString(), this);
aRemoveFromBuddyList = new QAction(QString(), this);
aAddToIgnoreList = new QAction(QString(), this);
aRemoveFromIgnoreList = new QAction(QString(), this);
aKick = new QAction(QString(), this);
aWarnUser = new QAction(QString(), this);
aWarnHistory = new QAction(QString(), this);
aBan = new QAction(QString(), this);
aBanHistory = new QAction(QString(), this);
aPromoteToMod = new QAction(QString(), this);
aDemoteFromMod = new QAction(QString(), this);
aPromoteToJudge = new QAction(QString(), this);
aDemoteFromJudge = new QAction(QString(), this);
aGetAdminNotes = new QAction(QString(), this);
retranslateUi();
}
void UserContextMenu::retranslateUi()
{
aDetails->setText(tr("User &details"));
aChat->setText(tr("Private &chat"));
aShowGames->setText(tr("Show this user's &games"));
aAddToBuddyList->setText(tr("Add to &buddy list"));
aRemoveFromBuddyList->setText(tr("Remove from &buddy list"));
aAddToIgnoreList->setText(tr("Add to &ignore list"));
aRemoveFromIgnoreList->setText(tr("Remove from &ignore list"));
aKick->setText(tr("Kick from &game"));
aWarnUser->setText(tr("Warn user"));
aWarnHistory->setText(tr("View user's war&n history"));
aBan->setText(tr("Ban from &server"));
aBanHistory->setText(tr("View user's &ban history"));
aPromoteToMod->setText(tr("&Promote user to moderator"));
aDemoteFromMod->setText(tr("Dem&ote user from moderator"));
aPromoteToJudge->setText(tr("Promote user to &judge"));
aDemoteFromJudge->setText(tr("Demote user from judge"));
aGetAdminNotes->setText(tr("View admin notes"));
}
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
{
const Command_GetGamesOfUser &cmd = commandContainer.session_command(0).GetExtension(Command_GetGamesOfUser::ext);
if (resp.response_code() == Response::RespNameNotFound) {
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Error"), tr("This user does not exist."));
return;
} else if (resp.response_code() == Response::RespInIgnoreList) {
QMessageBox::critical(
static_cast<QWidget *>(parent()), tr("Error"),
tr("You are being ignored by %1 and can't see their games.").arg(QString::fromStdString(cmd.user_name())));
return;
} else if (resp.response_code() != Response::RespOk) {
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Error"),
tr("Could not get %1's games.").arg(QString::fromStdString(cmd.user_name())));
return;
}
const Response_GetGamesOfUser &response = resp.GetExtension(Response_GetGamesOfUser::ext);
QMap<int, GameTypeMap> gameTypeMap;
QMap<int, QString> roomMap;
const int roomListSize = response.room_list_size();
for (int i = 0; i < roomListSize; ++i) {
const ServerInfo_Room &roomInfo = response.room_list(i);
roomMap.insert(roomInfo.room_id(), QString::fromStdString(roomInfo.name()));
GameTypeMap tempMap;
const int gameTypeListSize = roomInfo.gametype_list_size();
for (int j = 0; j < gameTypeListSize; ++j) {
const ServerInfo_GameType &gameTypeInfo = roomInfo.gametype_list(j);
tempMap.insert(gameTypeInfo.game_type_id(), QString::fromStdString(gameTypeInfo.description()));
}
gameTypeMap.insert(roomInfo.room_id(), tempMap);
}
GameSelector *selector = new GameSelector(client, tabSupervisor, nullptr, roomMap, gameTypeMap, false, false);
selector->setParent(static_cast<QWidget *>(parent()), Qt::Window);
const int gameListSize = response.game_list_size();
for (int i = 0; i < gameListSize; ++i) {
selector->processGameInfo(response.game_list(i));
}
selector->setWindowTitle(tr("%1's games").arg(QString::fromStdString(cmd.user_name())));
selector->setMinimumWidth(800);
selector->setAttribute(Qt::WA_DeleteOnClose);
selector->show();
}
void UserContextMenu::banUser_processUserInfoResponse(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
// The dialog needs to be non-modal in order to not block the event queue of the client.
BanDialog *dlg = new BanDialog(response.user_info(), static_cast<QWidget *>(parent()));
connect(dlg, &QDialog::accepted, this, &UserContextMenu::banUser_dialogFinished);
dlg->show();
}
void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r)
{
const Response_WarnList &response = r.GetExtension(Response_WarnList::ext);
QString user = QString::fromStdString(response.user_name()).simplified();
QString clientid = QString::fromStdString(response.user_clientid()).simplified();
// The dialog needs to be non-modal in order to not block the event queue of the client.
WarningDialog *dlg = new WarningDialog(user, clientid, static_cast<QWidget *>(parent()));
connect(dlg, &QDialog::accepted, this, &UserContextMenu::warnUser_dialogFinished);
if (response.warning_size() > 0) {
for (int i = 0; i < response.warning_size(); ++i) {
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified());
}
}
dlg->show();
}
void UserContextMenu::warnUser_processUserInfoResponse(const Response &resp)
{
const Response_GetUserInfo &response = resp.GetExtension(Response_GetUserInfo::ext);
ServerInfo_User userInfo = response.user_info();
Command_GetWarnList cmd;
cmd.set_user_name(userInfo.name());
cmd.set_user_clientid(userInfo.clientid());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processGetWarningsListResponse);
client->sendCommand(pend);
}
void UserContextMenu::banUserHistory_processResponse(const Response &resp)
{
const Response_BanHistory &response = resp.GetExtension(Response_BanHistory::ext);
if (resp.response_code() == Response::RespOk) {
if (response.ban_list_size() > 0) {
QTableWidget *table = new QTableWidget();
table->setWindowTitle(tr("Ban History"));
table->setRowCount(response.ban_list_size());
table->setColumnCount(5);
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
table->setHorizontalHeaderLabels(
QString(tr("Ban Time;Moderator;Ban Length;Ban Reason;Visible Reason")).split(";"));
ServerInfo_Ban ban;
for (int i = 0; i < response.ban_list_size(); ++i) {
ban = response.ban_list(i);
table->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
table->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
table->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
table->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
table->setItem(i, 4, new QTableWidgetItem(QString::fromStdString(ban.visible_reason())));
}
table->resizeColumnsToContents();
table->setMinimumSize(table->horizontalHeader()->length() + (table->columnCount() * 5),
table->verticalHeader()->length() + (table->rowCount() * 3));
table->show();
} else
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Ban History"),
tr("User has never been banned."));
} else
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Ban History"),
tr("Failed to collect ban information."));
}
void UserContextMenu::warnUserHistory_processResponse(const Response &resp)
{
const Response_WarnHistory &response = resp.GetExtension(Response_WarnHistory::ext);
if (resp.response_code() == Response::RespOk) {
if (response.warn_list_size() > 0) {
QTableWidget *table = new QTableWidget();
table->setWindowTitle(tr("Warning History"));
table->setRowCount(response.warn_list_size());
table->setColumnCount(4);
table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
table->setHorizontalHeaderLabels(QString(tr("Warning Time;Moderator;User Name;Reason")).split(";"));
ServerInfo_Warning warn;
for (int i = 0; i < response.warn_list_size(); ++i) {
warn = response.warn_list(i);
table->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
table->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
table->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.user_name())));
table->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(warn.reason())));
}
table->resizeColumnsToContents();
table->setMinimumSize(table->horizontalHeader()->length() + (table->columnCount() * 5),
table->verticalHeader()->length() + (table->rowCount() * 3));
table->show();
} else
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Warning History"),
tr("User has never been warned."));
} else
QMessageBox::critical(static_cast<QWidget *>(parent()), tr("Warning History"),
tr("Failed to collect warning information."));
}
void UserContextMenu::getAdminNotes_processResponse(const Response &resp)
{
const Response_GetAdminNotes &response = resp.GetExtension(Response_GetAdminNotes::ext);
if (resp.response_code() != Response::RespOk) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to get admin notes."));
return;
}
auto *dlg = new AdminNotesDialog(QString::fromStdString(response.user_name()),
QString::fromStdString(response.notes()), static_cast<QWidget *>(parent()));
connect(dlg, &AdminNotesDialog::accepted, this, &UserContextMenu::updateAdminNotes_dialogFinished);
dlg->show();
}
void UserContextMenu::adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer)
{
const Command_AdjustMod &cmd = commandContainer.admin_command(0).GetExtension(Command_AdjustMod::ext);
if (resp.response_code() == Response::RespOk) {
if (cmd.should_be_mod() || cmd.should_be_judge()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"),
tr("Successfully promoted user."));
} else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Success"), tr("Successfully demoted user."));
}
} else {
if (cmd.should_be_mod() || cmd.should_be_judge()) {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to promote user."));
} else {
QMessageBox::information(static_cast<QWidget *>(parent()), tr("Failed"), tr("Failed to demote user."));
}
}
}
void UserContextMenu::banUser_dialogFinished()
{
BanDialog *dlg = static_cast<BanDialog *>(sender());
Command_BanFromServer cmd;
cmd.set_user_name(dlg->getBanName().toStdString());
cmd.set_address(dlg->getBanIP().toStdString());
cmd.set_minutes(dlg->getMinutes());
cmd.set_reason(dlg->getReason().toStdString());
cmd.set_visible_reason(dlg->getVisibleReason().toStdString());
cmd.set_clientid(dlg->getBanId().toStdString());
int removeAmount = dlg->getDeleteMessages();
if (removeAmount != 0) {
cmd.set_remove_messages(removeAmount);
}
client->sendCommand(client->prepareModeratorCommand(cmd));
}
void UserContextMenu::warnUser_dialogFinished()
{
WarningDialog *dlg = static_cast<WarningDialog *>(sender());
if (dlg->getName().isEmpty() || userListProxy->getOwnUsername().simplified().isEmpty())
return;
Command_WarnUser cmd;
cmd.set_user_name(dlg->getName().toStdString());
cmd.set_reason(dlg->getReason().toStdString());
cmd.set_clientid(dlg->getWarnID().toStdString());
int removeAmount = dlg->getDeleteMessages();
if (removeAmount != 0) {
cmd.set_remove_messages(removeAmount);
}
client->sendCommand(client->prepareModeratorCommand(cmd));
}
void UserContextMenu::updateAdminNotes_dialogFinished()
{
auto *dlg = static_cast<AdminNotesDialog *>(sender());
Command_UpdateAdminNotes cmd;
cmd.set_user_name(dlg->getName().toStdString());
cmd.set_notes(dlg->getNotes().toStdString());
client->sendCommand(client->prepareModeratorCommand(cmd));
}
void UserContextMenu::showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
bool online,
int playerId)
{
showContextMenu(pos, userName, userLevel, online, playerId, QString(), nullptr);
}
void UserContextMenu::showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
ChatView *chatView)
{
showContextMenu(pos, userName, userLevel, true, -1, QString(), chatView);
}
void UserContextMenu::showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
bool online,
int playerId,
const QString &deckHash,
ChatView *chatView)
{
QAction *aCopyToClipBoard = nullptr, *aRemoveMessages = nullptr;
aUserName->setText(userName);
auto *menu = new QMenu(static_cast<QWidget *>(parent()));
menu->addAction(aUserName);
menu->addSeparator();
if (!deckHash.isEmpty()) {
aCopyToClipBoard = new QAction(tr("Copy hash to clipboard"), this);
menu->addAction(aCopyToClipBoard);
}
menu->addAction(aDetails);
menu->addAction(aShowGames);
menu->addAction(aChat);
if (userLevel.testFlag(ServerInfo_User::IsRegistered) && userListProxy->isOwnUserRegistered()) {
menu->addSeparator();
if (userListProxy->isUserBuddy(userName)) {
menu->addAction(aRemoveFromBuddyList);
} else {
menu->addAction(aAddToBuddyList);
}
if (userListProxy->isUserIgnored(userName)) {
menu->addAction(aRemoveFromIgnoreList);
} else {
menu->addAction(aAddToIgnoreList);
}
}
if (chatView != nullptr) {
aRemoveMessages = new QAction(tr("Remove this user's messages"), this);
menu->addAction(aRemoveMessages);
}
if (game && (game->isHost() || !tabSupervisor->getAdminLocked())) {
menu->addSeparator();
menu->addAction(aKick);
}
if (!tabSupervisor->getAdminLocked()) {
menu->addSeparator();
menu->addAction(aWarnUser);
menu->addAction(aWarnHistory);
menu->addSeparator();
menu->addAction(aBan);
menu->addAction(aBanHistory);
menu->addSeparator();
menu->addAction(aGetAdminNotes);
menu->addSeparator();
if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromMod);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToMod);
}
if (userLevel.testFlag(ServerInfo_User::IsJudge) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aDemoteFromJudge);
} else if (userLevel.testFlag(ServerInfo_User::IsRegistered) &&
(tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsAdmin)) {
menu->addAction(aPromoteToJudge);
}
}
bool anotherUser = userName != userListProxy->getOwnUsername();
aDetails->setEnabled(true);
aChat->setEnabled(anotherUser && online);
aShowGames->setEnabled(online);
aAddToBuddyList->setEnabled(anotherUser);
aRemoveFromBuddyList->setEnabled(anotherUser);
aAddToIgnoreList->setEnabled(anotherUser);
aRemoveFromIgnoreList->setEnabled(anotherUser);
aKick->setEnabled(anotherUser);
aWarnUser->setEnabled(anotherUser);
aWarnHistory->setEnabled(anotherUser);
aBan->setEnabled(anotherUser);
aBanHistory->setEnabled(anotherUser);
aGetAdminNotes->setEnabled(anotherUser);
aPromoteToMod->setEnabled(anotherUser);
aDemoteFromMod->setEnabled(anotherUser);
QAction *actionClicked = menu->exec(pos);
if (actionClicked == nullptr) {
} else if (actionClicked == aDetails) {
UserInfoBox *infoWidget =
new UserInfoBox(client, false, static_cast<QWidget *>(parent()),
Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint);
infoWidget->setAttribute(Qt::WA_DeleteOnClose);
infoWidget->updateInfo(userName);
} else if (actionClicked == aChat) {
emit openMessageDialog(userName, true);
} else if (actionClicked == aShowGames) {
Command_GetGamesOfUser cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::gamesOfUserReceived);
client->sendCommand(pend);
} else if (actionClicked == aAddToBuddyList) {
Command_AddToList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aRemoveFromBuddyList) {
Command_RemoveFromList cmd;
cmd.set_list("buddy");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aAddToIgnoreList) {
Command_AddToList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aRemoveFromIgnoreList) {
Command_RemoveFromList cmd;
cmd.set_list("ignore");
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
} else if (actionClicked == aKick) {
Command_KickFromGame cmd;
cmd.set_player_id(playerId);
game->getGameEventHandler()->sendGameCommand(cmd);
} else if (actionClicked == aBan) {
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUser_processUserInfoResponse);
client->sendCommand(pend);
} else if (actionClicked == aPromoteToMod || actionClicked == aDemoteFromMod) {
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_mod(actionClicked == aPromoteToMod);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
} else if (actionClicked == aPromoteToJudge || actionClicked == aDemoteFromJudge) {
Command_AdjustMod cmd;
cmd.set_user_name(userName.toStdString());
cmd.set_should_be_judge(actionClicked == aPromoteToJudge);
PendingCommand *pend = client->prepareAdminCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::adjustMod_processUserResponse);
client->sendCommand(pend);
} else if (actionClicked == aBanHistory) {
Command_GetBanHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::banUserHistory_processResponse);
client->sendCommand(pend);
} else if (actionClicked == aWarnUser) {
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUser_processUserInfoResponse);
client->sendCommand(pend);
} else if (actionClicked == aWarnHistory) {
Command_GetWarnHistory cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::warnUserHistory_processResponse);
client->sendCommand(pend);
} else if (actionClicked == aGetAdminNotes) {
Command_GetAdminNotes cmd;
cmd.set_user_name(userName.toStdString());
auto *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserContextMenu::getAdminNotes_processResponse);
client->sendCommand(pend);
} else if (actionClicked == aCopyToClipBoard) {
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setText(deckHash);
} else if (actionClicked == aRemoveMessages) {
chatView->redactMessages(userName, -1);
}
delete menu;
}

View file

@ -0,0 +1,79 @@
/**
* @file user_context_menu.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef USER_CONTEXT_MENU_H
#define USER_CONTEXT_MENU_H
#include <QObject>
#include <libcockatrice/network/server/remote/user_level.h>
class AbstractGame;
class UserListProxy;
class AbstractClient;
class ChatView;
class CommandContainer;
class QAction;
class QMenu;
class QPoint;
class Response;
class ServerInfo_User;
class TabSupervisor;
class UserContextMenu : public QObject
{
Q_OBJECT
private:
AbstractClient *client;
TabSupervisor *tabSupervisor;
const UserListProxy *userListProxy;
AbstractGame *game;
QAction *aUserName;
QAction *aDetails;
QAction *aShowGames;
QAction *aChat;
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
QAction *aKick;
QAction *aBan, *aBanHistory;
QAction *aPromoteToMod, *aDemoteFromMod;
QAction *aPromoteToJudge, *aDemoteFromJudge;
QAction *aWarnUser, *aWarnHistory;
QAction *aGetAdminNotes;
signals:
void openMessageDialog(const QString &userName, bool focus);
private slots:
void banUser_processUserInfoResponse(const Response &resp);
void warnUser_processGetWarningsListResponse(const Response &r);
void warnUser_processUserInfoResponse(const Response &resp);
void banUserHistory_processResponse(const Response &resp);
void warnUserHistory_processResponse(const Response &resp);
void getAdminNotes_processResponse(const Response &resp);
void adjustMod_processUserResponse(const Response &resp, const CommandContainer &commandContainer);
void banUser_dialogFinished();
void warnUser_dialogFinished();
void updateAdminNotes_dialogFinished();
void gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer);
public:
UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *_parent, AbstractGame *_game = 0);
void retranslateUi();
void showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
bool online = true,
int playerId = -1);
void showContextMenu(const QPoint &pos, const QString &userName, UserLevelFlags userLevel, ChatView *chatView);
void showContextMenu(const QPoint &pos,
const QString &userName,
UserLevelFlags userLevel,
bool online,
int playerId,
const QString &deckHash,
ChatView *chatView = nullptr);
};
#endif

View file

@ -0,0 +1,385 @@
#include "user_info_box.h"
#include "../../dialogs/dlg_edit_avatar.h"
#include "../../dialogs/dlg_edit_password.h"
#include "../../dialogs/dlg_edit_user.h"
#include "../../interface/pixel_map_generator.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include <QDateTime>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/passwordhasher.h>
UserInfoBox::UserInfoBox(AbstractClient *_client, bool _editable, QWidget *parent, Qt::WindowFlags flags)
: QWidget(parent, flags), client(_client), editable(_editable)
{
QFont nameFont = nameLabel.font();
nameFont.setBold(true);
nameFont.setPointSizeF(nameFont.pointSizeF() * 1.5);
nameLabel.setFont(nameFont);
auto *userIconAndNameLayout = new QHBoxLayout;
userIconAndNameLayout->addWidget(&userLevelIcon, 0, Qt::AlignCenter);
userIconAndNameLayout->addWidget(&nameLabel, 1);
userIconAndNameLayout->addStretch();
avatarPic.setMinimumSize(200, 200);
avatarPic.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
avatarPic.setAlignment(Qt::AlignCenter);
auto *avatarLayout = new QHBoxLayout;
avatarLayout->setContentsMargins(0, 0, 0, 0);
avatarLayout->addStretch(1);
avatarLayout->addWidget(&avatarPic, 3);
avatarLayout->addStretch(1);
auto *mainLayout = new QGridLayout;
mainLayout->addLayout(avatarLayout, 0, 0, 1, 3);
mainLayout->addLayout(userIconAndNameLayout, 1, 0, 1, 3);
mainLayout->addWidget(&realNameLabel1, 2, 0, 1, 1);
mainLayout->addWidget(&realNameLabel2, 2, 1, 1, 2);
mainLayout->addWidget(&countryLabel1, 3, 0, 1, 1);
mainLayout->addWidget(&countryLabel2, 3, 1, 1, 1, Qt::AlignCenter);
mainLayout->addWidget(&countryLabel3, 3, 2, 1, 1);
mainLayout->addWidget(&userLevelLabel1, 4, 0, 1, 1);
mainLayout->addWidget(&userLevelLabel2, 4, 1, 1, 2);
mainLayout->addWidget(&accountAgeLabel1, 5, 0, 1, 1);
mainLayout->addWidget(&accountAgeLabel2, 5, 1, 1, 2);
mainLayout->setColumnStretch(2, 10);
if (editable) {
auto *buttonsLayout = new QHBoxLayout;
buttonsLayout->addWidget(&editButton);
buttonsLayout->addWidget(&passwordButton);
buttonsLayout->addWidget(&avatarButton);
mainLayout->addLayout(buttonsLayout, 7, 0, 1, 3);
connect(&editButton, &QPushButton::clicked, this, &UserInfoBox::actEdit);
connect(&passwordButton, &QPushButton::clicked, this, &UserInfoBox::actPassword);
connect(&avatarButton, &QPushButton::clicked, this, &UserInfoBox::actAvatar);
}
setWindowTitle(tr("User Information"));
setLayout(mainLayout);
retranslateUi();
}
void UserInfoBox::retranslateUi()
{
realNameLabel1.setText(tr("Real Name:"));
countryLabel1.setText(tr("Location:"));
userLevelLabel1.setText(tr("User Level:"));
accountAgeLabel1.setText(tr("Account Age:"));
editButton.setText(tr("Edit"));
passwordButton.setText(tr("Change password"));
avatarButton.setText(tr("Change avatar"));
}
/**
* Creates the default profile pic that is used when the user doesn't have a custom pic
*/
static QPixmap createDefaultAvatar(int height, const ServerInfo_User &user)
{
return UserLevelPixmapGenerator::generatePixmap(height, UserLevelFlags(user.user_level()), user.pawn_colors(),
false, QString::fromStdString(user.privlevel()));
}
void UserInfoBox::updateInfo(const ServerInfo_User &user)
{
currentUserInfo = &user;
const UserLevelFlags userLevel(user.user_level());
const std::string &bmp = user.avatar_bmp();
if (!avatarPixmap.loadFromData((const uchar *)bmp.data(), static_cast<uint>(bmp.size()))) {
avatarPixmap = createDefaultAvatar(64, user);
hasAvatar = false;
} else {
hasAvatar = true;
}
nameLabel.setText(QString::fromStdString(user.name()));
realNameLabel2.setText(QString::fromStdString(user.real_name()));
QString country = QString::fromStdString(user.country());
if (country.length() != 0) {
countryLabel2.setPixmap(CountryPixmapGenerator::generatePixmap(13, country));
countryLabel3.setText(QString("%1").arg(country.toUpper()));
} else {
countryLabel2.setText("");
countryLabel3.setText("");
}
userLevelIcon.setPixmap(UserLevelPixmapGenerator::generatePixmap(15, userLevel, user.pawn_colors(), false,
QString::fromStdString(user.privlevel())));
QString userLevelText;
if (userLevel.testFlag(ServerInfo_User::IsAdmin))
userLevelText = tr("Administrator");
else if (userLevel.testFlag(ServerInfo_User::IsModerator))
userLevelText = tr("Moderator");
else if (userLevel.testFlag(ServerInfo_User::IsRegistered))
userLevelText = tr("Registered user");
else
userLevelText = tr("Unregistered user");
if (userLevel.testFlag(ServerInfo_User::IsJudge))
userLevelText += " | " + tr("Judge");
if (user.has_privlevel() && user.privlevel() != "NONE") {
userLevelText += " | " + QString("%1").arg(user.privlevel().c_str());
}
userLevelLabel2.setText(userLevelText);
QString accountAgeString = tr("Unregistered user");
if (userLevel.testFlag(ServerInfo_User::IsAdmin) || userLevel.testFlag(ServerInfo_User::IsModerator) ||
userLevel.testFlag(ServerInfo_User::IsRegistered)) {
accountAgeString = getAgeString(user.accountage_secs());
}
accountAgeLabel2.setText(accountAgeString);
}
QString UserInfoBox::getAgeString(int ageSeconds)
{
QString accountAgeString = tr("Unknown");
if (ageSeconds <= 0)
return accountAgeString;
// secsSinceEpoch is in utc
auto secsSinceEpoch = QDateTime::currentSecsSinceEpoch() - ageSeconds;
// the date is in local time, fromSecsSinceEpoch expects a timestamp from utc and converts it to local time
auto date = QDateTime::fromSecsSinceEpoch(secsSinceEpoch).date();
if (!date.isValid())
return accountAgeString;
// now can be local time as the date is also local time
auto now = QDate::currentDate();
auto daysAndYears = getDaysAndYearsBetween(date, now);
QString dateString = QLocale().toString(date, QLocale::ShortFormat);
QString yearString;
if (daysAndYears.second > 0) {
yearString = tr("%n Year(s), ", "amount of years (only shown if more than 0)", daysAndYears.second);
}
accountAgeString =
tr("%10%n Day(s) %20", "amount of years (if more than 0), amount of days, date in local short format",
daysAndYears.first)
.arg(yearString)
.arg(dateString);
return accountAgeString;
}
void UserInfoBox::updateInfo(const QString &userName)
{
Command_GetUserInfo cmd;
cmd.set_user_name(userName.toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processResponse);
client->sendCommand(pend);
}
void UserInfoBox::processResponse(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
updateInfo(response.user_info());
resize(sizeHint());
show();
}
void UserInfoBox::actEdit()
{
Command_GetUserInfo cmd;
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::actEditInternal);
client->sendCommand(pend);
}
void UserInfoBox::actEditInternal(const Response &r)
{
const Response_GetUserInfo &response = r.GetExtension(Response_GetUserInfo::ext);
const ServerInfo_User &user = response.user_info();
QString email = QString::fromStdString(user.email());
QString country = QString::fromStdString(user.country());
QString realName = QString::fromStdString(user.real_name());
DlgEditUser dlg(this, email, country, realName);
if (!dlg.exec())
return;
Command_AccountEdit cmd;
cmd.set_real_name(dlg.getRealName().toStdString());
if (client->getServerSupportsPasswordHash()) {
if (email != dlg.getEmail()) {
// real password is required to change email
bool ok = false;
QString password =
getTextWithMax(this, tr("Enter Password"),
tr("Password verification is required in order to change your email address"),
QLineEdit::Password, "", &ok);
if (!ok)
return;
cmd.set_password_check(password.toStdString());
cmd.set_email(dlg.getEmail().toStdString());
} // servers that support password hash do not require all fields to be filled anymore
} else {
cmd.set_email(dlg.getEmail().toStdString());
}
cmd.set_country(dlg.getCountry().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processEditResponse);
client->sendCommand(pend);
}
void UserInfoBox::actPassword()
{
DlgEditPassword dlg(this);
if (!dlg.exec())
return;
auto oldPassword = dlg.getOldPassword();
auto newPassword = dlg.getNewPassword();
if (client->getServerSupportsPasswordHash()) {
Command_RequestPasswordSalt cmd;
cmd.set_user_name(client->getUserName().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, [=, this](const Response &response) {
if (response.response_code() == Response::RespOk) {
changePassword(oldPassword, newPassword);
} else {
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
}
});
client->sendCommand(pend);
} else {
changePassword(oldPassword, newPassword);
}
}
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
{
Command_AccountPassword cmd;
cmd.set_old_password(oldPassword.toStdString());
if (client->getServerSupportsPasswordHash()) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt);
cmd.set_hashed_new_password(hashedPassword.toStdString());
} else {
cmd.set_new_password(newPassword.toStdString());
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processPasswordResponse);
client->sendCommand(pend);
}
void UserInfoBox::actAvatar()
{
DlgEditAvatar dlg(this);
if (!dlg.exec())
return;
Command_AccountImage cmd;
cmd.set_image(dlg.getImage().data(), dlg.getImage().size());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this, &UserInfoBox::processAvatarResponse);
client->sendCommand(pend);
}
void UserInfoBox::processEditResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
updateInfo(nameLabel.text());
QMessageBox::information(this, tr("Information"), tr("User information updated."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"),
tr("This server does not permit you to update your user informations."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("The entered password does not match your account."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
break;
}
}
void UserInfoBox::processPasswordResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
QMessageBox::information(this, tr("Information"), tr("Password changed."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"), tr("This server does not permit you to change your password."));
break;
case Response::RespPasswordTooShort:
QMessageBox::critical(this, tr("Error"), tr("The new password is too short."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("The old password is incorrect."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
break;
}
}
void UserInfoBox::processAvatarResponse(const Response &r)
{
switch (r.response_code()) {
case Response::RespOk:
updateInfo(nameLabel.text());
QMessageBox::information(this, tr("Information"), tr("Avatar updated."));
break;
case Response::RespFunctionNotAllowed:
QMessageBox::critical(this, tr("Error"), tr("This server does not permit you to update your avatar."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"), tr("An error occured while trying to updater your avatar."));
break;
}
}
void UserInfoBox::resizeEvent(QResizeEvent *event)
{
QPixmap resizedPixmap;
if (hasAvatar) {
resizedPixmap = avatarPixmap.scaled(avatarPic.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
int height = qMin(avatarPic.size().width(), avatarPic.size().height());
resizedPixmap = createDefaultAvatar(height, *currentUserInfo);
}
avatarPic.setPixmap(resizedPixmap);
QWidget::resizeEvent(event);
}

View file

@ -0,0 +1,63 @@
/**
* @file user_info_box.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef USERINFOBOX_H
#define USERINFOBOX_H
#include <QDateTime>
#include <QLabel>
#include <QPushButton>
#include <QWidget>
class ServerInfo_User;
class AbstractClient;
class Response;
class UserInfoBox : public QWidget
{
Q_OBJECT
private:
AbstractClient *client;
bool editable;
QLabel avatarPic, userLevelIcon, nameLabel, realNameLabel1, realNameLabel2, countryLabel1, countryLabel2,
countryLabel3, userLevelLabel1, userLevelLabel2, accountAgeLabel1, accountAgeLabel2;
QPushButton editButton, passwordButton, avatarButton;
QPixmap avatarPixmap;
bool hasAvatar;
const ServerInfo_User *currentUserInfo;
static QString getAgeString(int ageSeconds);
public:
UserInfoBox(AbstractClient *_client, bool editable, QWidget *parent = nullptr, Qt::WindowFlags flags = {});
void retranslateUi();
inline static QPair<int, int> getDaysAndYearsBetween(const QDate &then, const QDate &now)
{
int years = now.addDays(1 - then.dayOfYear()).year() - then.year(); // there is no yearsTo
int days = then.addYears(years).daysTo(now);
return {days, years};
}
private slots:
void processResponse(const Response &r);
void processEditResponse(const Response &r);
void processPasswordResponse(const Response &r);
void processAvatarResponse(const Response &r);
void actEdit();
void actEditInternal(const Response &r);
void actPassword();
void actAvatar();
public slots:
void updateInfo(const ServerInfo_User &user);
void updateInfo(const QString &userName);
private:
void resizeEvent(QResizeEvent *event) override;
void changePassword(const QString &oldPassword, const QString &newPassword);
};
#endif

View file

@ -0,0 +1,79 @@
#include "user_info_connection.h"
#include <QDebug>
#include <libcockatrice/settings/cache_settings.h>
#include <utility>
UserConnection_Information::UserConnection_Information() = default;
UserConnection_Information::UserConnection_Information(QString _saveName,
QString _serverName,
QString _portNum,
QString _userName,
QString _pass,
bool _savePass,
QString _site)
: saveName(std::move(_saveName)), server(std::move(_serverName)), port(std::move(_portNum)),
username(std::move(_userName)), password(std::move(_pass)), savePassword(_savePass), site(std::move(_site))
{
}
QMap<QString, std::pair<QString, UserConnection_Information>> UserConnection_Information::getServerInfo()
{
QMap<QString, std::pair<QString, UserConnection_Information>> serverList;
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
UserConnection_Information userInfo(_saveName, serverName, portNum, userName, pass, savePass, _site);
serverList.insert(_saveName, std::make_pair(serverName, userInfo));
}
return serverList;
}
QStringList UserConnection_Information::getServerInfo(const QString &find)
{
QStringList _server;
ServersSettings &servers = SettingsCache::instance().servers();
int size = servers.getValue("totalServers", "server", "server_details").toInt() + 1;
for (int i = 0; i < size; i++) {
QString _saveName = servers.getValue(QString("saveName%1").arg(i), "server", "server_details").toString();
if (find != _saveName)
continue;
QString serverName = servers.getValue(QString("server%1").arg(i), "server", "server_details").toString();
QString portNum = servers.getValue(QString("port%1").arg(i), "server", "server_details").toString();
QString userName = servers.getValue(QString("username%1").arg(i), "server", "server_details").toString();
QString pass = servers.getValue(QString("password%1").arg(i), "server", "server_details").toString();
bool savePass = servers.getValue(QString("savePassword%1").arg(i), "server", "server_details").toBool();
QString _site = servers.getValue(QString("site%1").arg(i), "server", "server_details").toString();
_server.append(_saveName);
_server.append(serverName);
_server.append(portNum);
_server.append(userName);
_server.append(pass);
_server.append(savePass ? "1" : "0");
_server.append(_site);
break;
}
if (_server.empty())
qCWarning(UserInfoConnectionLog) << "There was a problem!";
return _server;
}

View file

@ -0,0 +1,64 @@
/**
* @file user_info_connection.h
* @ingroup Client
* @brief TODO: Document this.
*/
#ifndef USERCONNECTION_INFORMATION_H
#define USERCONNECTION_INFORMATION_H
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QLoggingCategory>
#include <QSettings>
#include <QStandardPaths>
inline Q_LOGGING_CATEGORY(UserInfoConnectionLog, "user_info_connection");
class UserConnection_Information
{
private:
QString saveName;
QString server;
QString port;
QString username;
QString password;
bool savePassword;
QString site;
public:
UserConnection_Information();
UserConnection_Information(QString, QString, QString, QString, QString, bool, QString);
QString getSaveName() const
{
return saveName;
}
QString getServer() const
{
return server;
}
QString getPort() const
{
return port;
}
QString getUsername() const
{
return username;
}
QString getPassword() const
{
return password;
}
bool getSavePassword() const
{
return savePassword;
}
QString getSite() const
{
return site;
}
QMap<QString, std::pair<QString, UserConnection_Information>> getServerInfo();
QStringList getServerInfo(const QString &find);
};
#endif

View file

@ -0,0 +1,170 @@
#include "user_list_manager.h"
#include "../../client/sound_engine.h"
#include "user_info_box.h"
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/event_add_to_list.pb.h>
#include <libcockatrice/protocol/pb/event_remove_from_list.pb.h>
#include <libcockatrice/protocol/pb/event_user_joined.pb.h>
#include <libcockatrice/protocol/pb/event_user_left.pb.h>
#include <libcockatrice/protocol/pb/response_list_users.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
UserListManager::UserListManager(AbstractClient *_client, QObject *parent)
: QObject(parent), client(_client), ownUserInfo(nullptr)
{
connect(client, &AbstractClient::userJoinedEventReceived, this, &UserListManager::processUserJoinedEvent);
connect(client, &AbstractClient::userLeftEventReceived, this, &UserListManager::processUserLeftEvent);
connect(client, &AbstractClient::buddyListReceived, this, &UserListManager::buddyListReceived);
connect(client, &AbstractClient::ignoreListReceived, this, &UserListManager::ignoreListReceived);
connect(client, &AbstractClient::addToListEventReceived, this, &UserListManager::processAddToListEvent);
connect(client, &AbstractClient::removeFromListEventReceived, this, &UserListManager::processRemoveFromListEvent);
connect(client, &AbstractClient::userInfoChanged, this, &UserListManager::setOwnUserInfo);
}
UserListManager::~UserListManager()
{
handleDisconnect();
}
void UserListManager::handleConnect()
{
populateInitialOnlineUsers();
}
void UserListManager::handleDisconnect()
{
onlineUsers.clear();
buddyUsers.clear();
ignoredUsers.clear();
delete ownUserInfo;
ownUserInfo = nullptr;
}
void UserListManager::setOwnUserInfo(const ServerInfo_User &userInfo)
{
ownUserInfo = new ServerInfo_User(userInfo);
}
void UserListManager::populateInitialOnlineUsers()
{
PendingCommand *pend = client->prepareSessionCommand(Command_ListUsers());
connect(pend, &PendingCommand::finished, this, &UserListManager::processListUsersResponse);
client->sendCommand(pend);
}
void UserListManager::processListUsersResponse(const Response &response)
{
const auto &resp = response.GetExtension(Response_ListUsers::ext);
const int userListSize = resp.user_list_size();
for (int i = 0; i < userListSize; ++i) {
const ServerInfo_User &info = resp.user_list(i);
const QString &userName = QString::fromStdString(info.name());
onlineUsers.insert(userName, info);
}
}
void UserListManager::processUserJoinedEvent(const Event_UserJoined &event)
{
const auto &info = event.user_info();
const QString &userName = QString::fromStdString(info.name());
onlineUsers.insert(userName, info);
}
void UserListManager::processUserLeftEvent(const Event_UserLeft &event)
{
const auto &userName = QString::fromStdString(event.name());
onlineUsers.remove(userName);
}
void UserListManager::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
{
for (const auto &user : _buddyList) {
const auto &userName = QString::fromStdString(user.name());
buddyUsers.insert(userName, user);
}
}
void UserListManager::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
{
for (const auto &user : _ignoreList) {
const auto &userName = QString::fromStdString(user.name());
ignoredUsers.insert(userName, user);
}
}
void UserListManager::processAddToListEvent(const Event_AddToList &event)
{
const auto &user = event.user_info();
const auto &userName = QString::fromStdString(user.name());
const auto &userListType = QString::fromStdString(event.list_name());
QMap<QString, ServerInfo_User> *userMap;
if (userListType == "buddy") {
userMap = &buddyUsers;
} else if (userListType == "ignore") {
userMap = &ignoredUsers;
} else {
return;
}
userMap->insert(userName, user);
}
void UserListManager::processRemoveFromListEvent(const Event_RemoveFromList &event)
{
const auto &userListType = QString::fromStdString(event.list_name());
const auto &userName = QString::fromStdString(event.user_name());
QMap<QString, ServerInfo_User> *userMap;
if (userListType == "buddy") {
userMap = &buddyUsers;
} else if (userListType == "ignore") {
userMap = &ignoredUsers;
} else {
return;
}
userMap->remove(userName);
}
bool UserListManager::isOwnUserRegistered() const
{
return ownUserInfo != nullptr && (ownUserInfo->user_level() & ServerInfo_User::IsRegistered) != 0;
}
QString UserListManager::getOwnUsername() const
{
return ownUserInfo != nullptr ? QString::fromStdString(ownUserInfo->name()) : QString();
}
bool UserListManager::isUserBuddy(const QString &userName) const
{
return buddyUsers.contains(userName);
}
bool UserListManager::isUserIgnored(const QString &userName) const
{
return ignoredUsers.contains(userName);
}
const ServerInfo_User *UserListManager::getOnlineUser(const QString &userName) const
{
const QString &userNameToMatchLower = userName.toLower();
const auto it =
std::find_if(onlineUsers.begin(), onlineUsers.end(), [&userNameToMatchLower](const ServerInfo_User &user) {
return userNameToMatchLower == QString::fromStdString(user.name()).toLower();
});
if (it != onlineUsers.end()) {
return &*it;
}
return nullptr;
};

View file

@ -0,0 +1,78 @@
/**
* @file user_list_manager.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef COCKATRICE_USER_LIST_MANAGER_H
#define COCKATRICE_USER_LIST_MANAGER_H
#include "user_list_proxy.h"
#include <QMap>
#include <QWidget>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
class AbstractClient;
class Event_AddToList;
class Event_ListRooms;
class Event_RemoveFromList;
class Event_UserJoined;
class Event_UserLeft;
class Response;
class ServerInfo_User;
class TabSupervisor;
class UserListManager : public QObject, public UserListProxy
{
Q_OBJECT
private:
AbstractClient *client;
ServerInfo_User *ownUserInfo;
QMap<QString, ServerInfo_User> onlineUsers, buddyUsers, ignoredUsers;
private slots:
void setOwnUserInfo(const ServerInfo_User &userInfo);
void populateInitialOnlineUsers();
void processListUsersResponse(const Response &response);
void processUserJoinedEvent(const Event_UserJoined &event);
void processUserLeftEvent(const Event_UserLeft &event);
void buddyListReceived(const QList<ServerInfo_User> &_buddyList);
void ignoreListReceived(const QList<ServerInfo_User> &_ignoreList);
void processAddToListEvent(const Event_AddToList &event);
void processRemoveFromListEvent(const Event_RemoveFromList &event);
public:
explicit UserListManager(AbstractClient *_client, QObject *parent = nullptr);
~UserListManager() override;
[[nodiscard]] QMap<QString, ServerInfo_User> getAllUsersList() const
{
return onlineUsers;
}
[[nodiscard]] QMap<QString, ServerInfo_User> getBuddyList() const
{
return buddyUsers;
}
[[nodiscard]] QMap<QString, ServerInfo_User> getIgnoreList() const
{
return ignoredUsers;
}
bool isOwnUserRegistered() const override;
QString getOwnUsername() const override;
bool isUserBuddy(const QString &userName) const override;
bool isUserIgnored(const QString &userName) const override;
const ServerInfo_User *getOnlineUser(const QString &userName) const override;
public slots:
void handleConnect();
void handleDisconnect();
signals:
void userLeft(const QString &userName);
void userJoined(const ServerInfo_User &userInfo);
};
#endif // COCKATRICE_USER_LIST_MANAGER_H

View file

@ -0,0 +1,28 @@
/**
* @file user_list_proxy.h
* @ingroup UI
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef COCKATRICE_USERLISTPROXY_H
#define COCKATRICE_USERLISTPROXY_H
class QString;
class ServerInfo_User;
/**
* Responsible for providing a bare-bones minimal interface into userlist information,
* including your current connection to the server as well as buddy/ignore/alluser lists.
*/
class UserListProxy
{
public:
virtual bool isOwnUserRegistered() const = 0;
virtual QString getOwnUsername() const = 0;
virtual bool isUserBuddy(const QString &userName) const = 0;
virtual bool isUserIgnored(const QString &userName) const = 0;
virtual const ServerInfo_User *getOnlineUser(const QString &userName) const = 0; // Can return nullptr
};
#endif // COCKATRICE_USERLISTPROXY_H

View file

@ -0,0 +1,539 @@
#include "user_list_widget.h"
#include "../../interface/pixel_map_generator.h"
#include "../../tabs/tab_account.h"
#include "../../tabs/tab_supervisor.h"
#include "../game_selector.h"
#include "user_context_menu.h"
#include "user_list_manager.h"
#include <QApplication>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QSpinBox>
#include <QVBoxLayout>
#include <QWidget>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
#include <libcockatrice/protocol/pb/response_get_games_of_user.pb.h>
#include <libcockatrice/protocol/pb/response_get_user_info.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/trice_limits.h>
BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent)
{
setAttribute(Qt::WA_DeleteOnClose);
nameBanCheckBox = new QCheckBox(tr("ban &user name"));
nameBanCheckBox->setChecked(true);
nameBanEdit = new QLineEdit(QString::fromStdString(info.name()));
nameBanEdit->setMaxLength(MAX_NAME_LENGTH);
ipBanCheckBox = new QCheckBox(tr("ban &IP address"));
ipBanCheckBox->setChecked(true);
ipBanEdit = new QLineEdit(QString::fromStdString(info.address()));
ipBanEdit->setMaxLength(MAX_NAME_LENGTH);
idBanCheckBox = new QCheckBox(tr("ban client I&D"));
idBanCheckBox->setChecked(true);
idBanEdit = new QLineEdit(QString::fromStdString(info.clientid()));
idBanEdit->setMaxLength(MAX_NAME_LENGTH);
if (QString::fromStdString(info.clientid()).isEmpty())
idBanCheckBox->setChecked(false);
QGridLayout *banTypeGrid = new QGridLayout;
banTypeGrid->addWidget(nameBanCheckBox, 0, 0);
banTypeGrid->addWidget(nameBanEdit, 0, 1);
banTypeGrid->addWidget(ipBanCheckBox, 1, 0);
banTypeGrid->addWidget(ipBanEdit, 1, 1);
banTypeGrid->addWidget(idBanCheckBox, 2, 0);
banTypeGrid->addWidget(idBanEdit, 2, 1);
QGroupBox *banTypeGroupBox = new QGroupBox(tr("Ban type"));
banTypeGroupBox->setLayout(banTypeGrid);
permanentRadio = new QRadioButton(tr("&permanent ban"));
temporaryRadio = new QRadioButton(tr("&temporary ban"));
temporaryRadio->setChecked(true);
connect(temporaryRadio, &QRadioButton::toggled, this, &BanDialog::enableTemporaryEdits);
daysLabel = new QLabel(tr("&Days:"));
daysEdit = new QSpinBox;
daysEdit->setMinimum(0);
daysEdit->setValue(0);
daysEdit->setMaximum(10000);
daysLabel->setBuddy(daysEdit);
hoursLabel = new QLabel(tr("&Hours:"));
hoursEdit = new QSpinBox;
hoursEdit->setMinimum(0);
hoursEdit->setValue(0);
hoursEdit->setMaximum(24);
hoursLabel->setBuddy(hoursEdit);
minutesLabel = new QLabel(tr("&Minutes:"));
minutesEdit = new QSpinBox;
minutesEdit->setMinimum(0);
minutesEdit->setValue(5);
minutesEdit->setMaximum(60);
minutesLabel->setBuddy(minutesEdit);
QGridLayout *durationLayout = new QGridLayout;
durationLayout->addWidget(permanentRadio, 0, 0, 1, 6);
durationLayout->addWidget(temporaryRadio, 1, 0, 1, 6);
durationLayout->addWidget(daysLabel, 2, 0);
durationLayout->addWidget(daysEdit, 2, 1);
durationLayout->addWidget(hoursLabel, 2, 2);
durationLayout->addWidget(hoursEdit, 2, 3);
durationLayout->addWidget(minutesLabel, 2, 4);
durationLayout->addWidget(minutesEdit, 2, 5);
QGroupBox *durationGroupBox = new QGroupBox(tr("Duration of the ban"));
durationGroupBox->setLayout(durationLayout);
QLabel *reasonLabel = new QLabel(tr("Please enter the reason for the ban.\nThis is only saved for moderators and "
"cannot be seen by the banned person."));
reasonEdit = new QPlainTextEdit;
QLabel *visibleReasonLabel =
new QLabel(tr("Please enter the reason for the ban that will be visible to the banned person."));
visibleReasonEdit = new QPlainTextEdit;
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
QPushButton *okButton = new QPushButton(tr("&OK"));
okButton->setAutoDefault(true);
connect(okButton, &QPushButton::clicked, this, &BanDialog::okClicked);
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
connect(cancelButton, &QPushButton::clicked, this, &BanDialog::reject);
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(banTypeGroupBox);
vbox->addWidget(durationGroupBox);
vbox->addWidget(reasonLabel);
vbox->addWidget(reasonEdit);
vbox->addWidget(visibleReasonLabel);
vbox->addWidget(visibleReasonEdit);
vbox->addWidget(deleteMessages);
vbox->addLayout(buttonLayout);
setLayout(vbox);
setWindowTitle(tr("Ban user from server"));
}
WarningDialog::WarningDialog(const QString userName, const QString clientID, QWidget *parent) : QDialog(parent)
{
setAttribute(Qt::WA_DeleteOnClose);
descriptionLabel = new QLabel(tr("Which warning would you like to send?"));
nameWarning = new QLineEdit(userName);
nameWarning->setMaxLength(MAX_NAME_LENGTH);
warnClientID = new QLineEdit(clientID);
warnClientID->setMaxLength(MAX_NAME_LENGTH);
warningOption = new QComboBox();
warningOption->addItem("");
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
QPushButton *okButton = new QPushButton(tr("&OK"));
okButton->setAutoDefault(true);
connect(okButton, &QPushButton::clicked, this, &WarningDialog::okClicked);
QPushButton *cancelButton = new QPushButton(tr("&Cancel"));
connect(cancelButton, &QPushButton::clicked, this, &WarningDialog::reject);
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(descriptionLabel);
vbox->addWidget(nameWarning);
vbox->addWidget(warningOption);
vbox->addWidget(deleteMessages);
vbox->addLayout(buttonLayout);
setLayout(vbox);
setWindowTitle(tr("Warn user for misconduct"));
}
void WarningDialog::okClicked()
{
if (nameWarning->text().simplified().isEmpty()) {
QMessageBox::critical(this, tr("Error"),
tr("User name to send a warning to can not be blank, please specify a user to warn."));
return;
}
if (warningOption->currentText().simplified().isEmpty()) {
QMessageBox::critical(this, tr("Error"),
tr("Warning to use can not be blank, please select a valid warning to send."));
return;
}
accept();
}
QString WarningDialog::getName() const
{
return nameWarning->text().simplified();
}
QString WarningDialog::getWarnID() const
{
return warnClientID->text().simplified();
}
QString WarningDialog::getReason() const
{
return warningOption->currentText().simplified();
}
int WarningDialog::getDeleteMessages() const
{
return deleteMessages->isChecked() ? -1 : 0;
}
void WarningDialog::addWarningOption(const QString warning)
{
warningOption->addItem(warning);
}
void BanDialog::okClicked()
{
if (!nameBanCheckBox->isChecked() && !ipBanCheckBox->isChecked() && !idBanCheckBox->isChecked()) {
QMessageBox::critical(this, tr("Error"),
tr("You have to select a name-based, IP-based, clientId based, or some combination of "
"the three to place a ban."));
return;
}
if (nameBanCheckBox->isChecked())
if (nameBanEdit->text().simplified() == "") {
QMessageBox::critical(this, tr("Error"),
tr("You must have a value in the name ban when selecting the name ban checkbox."));
return;
}
if (ipBanCheckBox->isChecked())
if (ipBanEdit->text().simplified() == "") {
QMessageBox::critical(this, tr("Error"),
tr("You must have a value in the ip ban when selecting the ip ban checkbox."));
return;
}
if (idBanCheckBox->isChecked())
if (idBanEdit->text().simplified() == "") {
QMessageBox::critical(
this, tr("Error"),
tr("You must have a value in the clientid ban when selecting the clientid ban checkbox."));
return;
}
accept();
}
void BanDialog::enableTemporaryEdits(bool enabled)
{
daysLabel->setEnabled(enabled);
daysEdit->setEnabled(enabled);
hoursLabel->setEnabled(enabled);
hoursEdit->setEnabled(enabled);
minutesLabel->setEnabled(enabled);
minutesEdit->setEnabled(enabled);
}
QString BanDialog::getBanId() const
{
return idBanCheckBox->isChecked() ? idBanEdit->text() : QString();
}
QString BanDialog::getBanName() const
{
return nameBanCheckBox->isChecked() ? nameBanEdit->text() : QString();
}
QString BanDialog::getBanIP() const
{
return ipBanCheckBox->isChecked() ? ipBanEdit->text() : QString();
}
int BanDialog::getMinutes() const
{
return permanentRadio->isChecked() ? 0
: (daysEdit->value() * 24 * 60 + hoursEdit->value() * 60 + minutesEdit->value());
}
QString BanDialog::getReason() const
{
return reasonEdit->toPlainText();
}
QString BanDialog::getVisibleReason() const
{
return visibleReasonEdit->toPlainText();
}
int BanDialog::getDeleteMessages() const
{
return deleteMessages->isChecked() ? -1 : 0;
}
AdminNotesDialog::AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent)
: QDialog(_parent), userName(_userName)
{
setAttribute(Qt::WA_DeleteOnClose);
auto *updateButton = new QPushButton(tr("Update Notes"));
updateButton->setEnabled(false);
connect(updateButton, &QPushButton::clicked, this, &AdminNotesDialog::accept);
notes = new QPlainTextEdit(_notes);
notes->setMinimumWidth(500);
connect(notes, &QPlainTextEdit::textChanged, this, [=]() { updateButton->setEnabled(true); });
auto *vbox = new QVBoxLayout;
vbox->addWidget(notes);
vbox->addWidget(updateButton);
setLayout(vbox);
setWindowTitle(tr("Admin Notes for %1").arg(_userName));
}
QString AdminNotesDialog::getNotes() const
{
return notes->toPlainText();
}
UserListItemDelegate::UserListItemDelegate(QObject *const parent) : QStyledItemDelegate(parent)
{
}
bool UserListItemDelegate::editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::RightButton) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
static_cast<UserListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
#else
static_cast<UserListWidget *>(parent())->showContextMenu(mouseEvent->globalPos(), index);
#endif
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
UserListTWI::UserListTWI(const ServerInfo_User &_userInfo) : QTreeWidgetItem(Type)
{
setUserInfo(_userInfo);
}
void UserListTWI::setUserInfo(const ServerInfo_User &_userInfo)
{
userInfo = _userInfo;
setData(0, Qt::UserRole, userInfo.user_level());
setIcon(0, UserLevelPixmapGenerator::generateIcon(18, UserLevelFlags(userInfo.user_level()), userInfo.pawn_colors(),
false, QString::fromStdString(userInfo.privlevel())));
setIcon(1, QIcon(CountryPixmapGenerator::generatePixmap(18, QString::fromStdString(userInfo.country()))));
setData(2, Qt::UserRole, QString::fromStdString(userInfo.name()));
setData(2, Qt::DisplayRole, QString::fromStdString(userInfo.name()));
setData(3, Qt::InitialSortOrderRole, QString::fromStdString(userInfo.privlevel()));
}
void UserListTWI::setOnline(bool online)
{
setData(0, Qt::UserRole + 1, online);
setData(2, Qt::ForegroundRole, online ? qApp->palette().brush(QPalette::WindowText) : QBrush(Qt::gray));
}
/**
* Sort Users in the following order
* 1) Online Users > Offline Users
* 2) Admins, judge/vip/donator status ignored
* 3) Moderators, judge/vip/donator status ignored
* 4) Judges
* 5) VIPs
* 6) Donators
* 7) Everyone else
* @param other RHS to compare to
* @return Left is less than the Right
*/
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// Sort by online/offline
if (data(0, Qt::UserRole + 1) != other.data(0, Qt::UserRole + 1)) {
return data(0, Qt::UserRole + 1).toBool();
}
const auto &lhsUserLevelFlags = UserLevelFlags(data(0, Qt::UserRole).toInt());
const auto &rhsUserLevelFlags = UserLevelFlags(other.data(0, Qt::UserRole).toInt());
// Admins & Mods need no additional comparison checks, just to see if they're an admin or a moderator
static const QList<ServerInfo_User_UserLevelFlag> userLevelWithNoOtherPrefOrder = {
ServerInfo_User_UserLevelFlag_IsAdmin, ServerInfo_User_UserLevelFlag_IsModerator};
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(),
other.data(2, Qt::UserRole).toString()) < 0;
} else if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) {
return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry);
}
}
// Judges can be sorted by their additional ranks
static const QList<ServerInfo_User_UserLevelFlag> userLevelOrder = {ServerInfo_User_UserLevelFlag_IsJudge,
ServerInfo_User_UserLevelFlag_IsRegistered,
ServerInfo_User_UserLevelFlag_IsUser};
for (const auto &userLevelEntry : userLevelOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) {
return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry);
}
}
// Sort by VIP > Donator > None
static const QMap<QString, int> privilegeOrder = {{"VIP", 3}, {"DONATOR", 2}, {"NONE", 1}, {"UNKNOWN", 0}};
const auto &lhsUserPrivLevel = privilegeOrder.value(data(3, Qt::InitialSortOrderRole).toString(), 0);
const auto &rhsUserPrivLevel = privilegeOrder.value(other.data(3, Qt::InitialSortOrderRole).toString(), 0);
if (lhsUserPrivLevel != rhsUserPrivLevel) {
return lhsUserPrivLevel > rhsUserPrivLevel;
}
// Sort by name
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
}
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
UserListType _type,
QWidget *parent)
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
{
itemDelegate = new UserListItemDelegate(this);
userContextMenu = new UserContextMenu(tabSupervisor, this);
connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &UserListWidget::openMessageDialog);
userTree = new QTreeWidget;
userTree->setColumnCount(3);
userTree->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
userTree->header()->setMinimumSectionSize(0);
userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 18));
userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true);
connect(userTree, &QTreeWidget::itemActivated, this, &UserListWidget::userClicked);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(userTree);
setLayout(vbox);
retranslateUi();
}
void UserListWidget::retranslateUi()
{
userContextMenu->retranslateUi();
switch (type) {
case AllUsersList:
titleStr = tr("Users connected to server: %1");
break;
case RoomList:
titleStr = tr("Users in this room: %1");
break;
case BuddyList:
titleStr = tr("Buddies online: %1 / %2");
break;
case IgnoreList:
titleStr = tr("Ignored users online: %1 / %2");
break;
}
updateCount();
}
void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online)
{
const QString userName = QString::fromStdString(user.name());
UserListTWI *item = users.value(userName);
if (item)
item->setUserInfo(user);
else {
item = new UserListTWI(user);
users.insert(userName, item);
userTree->addTopLevelItem(item);
if (online)
++onlineCount;
updateCount();
}
item->setOnline(online);
}
bool UserListWidget::deleteUser(const QString &userName)
{
UserListTWI *twi = users.value(userName);
if (twi) {
users.remove(userName);
userTree->takeTopLevelItem(userTree->indexOfTopLevelItem(twi));
if (twi->data(0, Qt::UserRole + 1).toBool())
--onlineCount;
delete twi;
updateCount();
return true;
}
return false;
}
void UserListWidget::setUserOnline(const QString &userName, bool online)
{
UserListTWI *twi = users.value(userName);
if (!twi)
return;
twi->setOnline(online);
if (online)
++onlineCount;
else
--onlineCount;
updateCount();
}
void UserListWidget::updateCount()
{
QString str = titleStr;
if ((type == BuddyList) || (type == IgnoreList))
str = str.arg(onlineCount);
setTitle(str.arg(userTree->topLevelItemCount()));
}
void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
{
emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true);
}
void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index)
{
const ServerInfo_User &userInfo = static_cast<UserListTWI *>(userTree->topLevelItem(index.row()))->getUserInfo();
bool online = index.sibling(index.row(), 0).data(Qt::UserRole + 1).toBool();
userContextMenu->showContextMenu(pos, QString::fromStdString(userInfo.name()),
UserLevelFlags(userInfo.user_level()), online);
}
void UserListWidget::sortItems()
{
userTree->sortItems(1, Qt::AscendingOrder);
}

View file

@ -0,0 +1,170 @@
/**
* @file user_list_widget.h
* @ingroup Lobby
* @brief TODO: Document this.
*/
#ifndef USERLIST_H
#define USERLIST_H
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QStyledItemDelegate>
#include <QTextEdit>
#include <QTreeWidgetItem>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
class QTreeWidget;
class ServerInfo_User;
class AbstractClient;
class TabSupervisor;
class QLabel;
class QCheckBox;
class QSpinBox;
class QRadioButton;
class QPlainTextEdit;
class Response;
class CommandContainer;
class UserContextMenu;
class BanDialog : public QDialog
{
Q_OBJECT
private:
QLabel *daysLabel, *hoursLabel, *minutesLabel;
QCheckBox *nameBanCheckBox, *ipBanCheckBox, *idBanCheckBox, *deleteMessages;
QLineEdit *nameBanEdit, *ipBanEdit, *idBanEdit;
QSpinBox *daysEdit, *hoursEdit, *minutesEdit;
QRadioButton *permanentRadio, *temporaryRadio;
QPlainTextEdit *reasonEdit, *visibleReasonEdit;
private slots:
void okClicked();
void enableTemporaryEdits(bool enabled);
public:
explicit BanDialog(const ServerInfo_User &info, QWidget *parent = nullptr);
QString getBanName() const;
QString getBanIP() const;
QString getBanId() const;
int getMinutes() const;
QString getReason() const;
QString getVisibleReason() const;
int getDeleteMessages() const;
};
class WarningDialog : public QDialog
{
Q_OBJECT
private:
QLabel *descriptionLabel;
QLineEdit *nameWarning;
QComboBox *warningOption;
QLineEdit *warnClientID;
QCheckBox *deleteMessages;
private slots:
void okClicked();
public:
WarningDialog(const QString userName, const QString clientID, QWidget *parent = nullptr);
QString getName() const;
QString getWarnID() const;
QString getReason() const;
int getDeleteMessages() const;
void addWarningOption(const QString warning);
};
class AdminNotesDialog : public QDialog
{
Q_OBJECT
private:
QString userName;
QPlainTextEdit *notes;
public:
explicit AdminNotesDialog(const QString &_userName, const QString &_notes, QWidget *_parent = nullptr);
QString getName() const
{
return userName;
}
QString getNotes() const;
};
class UserListItemDelegate : public QStyledItemDelegate
{
public:
explicit UserListItemDelegate(QObject *const parent);
bool editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
};
class UserListTWI : public QTreeWidgetItem
{
private:
ServerInfo_User userInfo;
public:
explicit UserListTWI(const ServerInfo_User &_userInfo);
const ServerInfo_User &getUserInfo() const
{
return userInfo;
}
void setUserInfo(const ServerInfo_User &_userInfo);
void setOnline(bool online);
bool operator<(const QTreeWidgetItem &other) const override;
};
class UserListWidget : public QGroupBox
{
Q_OBJECT
public:
enum UserListType
{
AllUsersList,
RoomList,
BuddyList,
IgnoreList
};
private:
QMap<QString, UserListTWI *> users;
TabSupervisor *tabSupervisor;
AbstractClient *client;
UserListType type;
QTreeWidget *userTree;
UserListItemDelegate *itemDelegate;
UserContextMenu *userContextMenu;
int onlineCount;
QString titleStr;
void updateCount();
private slots:
void userClicked(QTreeWidgetItem *item, int column);
signals:
void openMessageDialog(const QString &userName, bool focus);
void addBuddy(const QString &userName);
void removeBuddy(const QString &userName);
void addIgnore(const QString &userName);
void removeIgnore(const QString &userName);
public:
UserListWidget(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
UserListType _type,
QWidget *parent = nullptr);
void retranslateUi();
void processUserInfo(const ServerInfo_User &user, bool online);
bool deleteUser(const QString &userName);
void setUserOnline(const QString &userName, bool online);
const QMap<QString, UserListTWI *> &getUsers() const
{
return users;
}
void showContextMenu(const QPoint &pos, const QModelIndex &index);
void sortItems();
};
#endif