Merge branch 'master' into tooomm-qt5

This commit is contained in:
tooomm 2026-08-27 06:33:34 +02:00
commit 3bc08ef94c
357 changed files with 23069 additions and 2916 deletions

View file

@ -2,7 +2,9 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../intents/intent_join_server_game.h"
#include "../interface/pixel_map_generator.h"
#include "../interface/widgets/server/game_link.h"
#include "../interface/widgets/server/user/user_list_manager.h"
#include "../interface/widgets/server/user/user_list_widget.h"
#include "../main.h"
@ -17,7 +19,9 @@
#include "tab_home.h"
#include "tab_logs.h"
#include "tab_message.h"
#include "tab_moderation.h"
#include "tab_replays.h"
#include "tab_report.h"
#include "tab_room.h"
#include "tab_server.h"
#include "tab_visual_database_display.h"
@ -30,6 +34,7 @@
#include <QPainter>
#include <QSystemTrayIcon>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
#include <libcockatrice/protocol/pb/event_notify_user.pb.h>
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
@ -38,6 +43,7 @@
#include <libcockatrice/protocol/pb/room_event.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/chat_settings.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/interface_settings.h>
@ -100,7 +106,7 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
tabLog(nullptr), isLocalGame(false)
tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
{
setElideMode(Qt::ElideRight);
setMovable(true);
@ -117,6 +123,7 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
connect(client, &AbstractClient::pingStatsUpdated, this, &TabSupervisor::updateLatencyTooltip);
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
// create tabs menu actions
@ -171,6 +178,14 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
aTabLog->setCheckable(true);
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
aTabReport = new QAction(this);
aTabReport->setCheckable(true);
connect(aTabReport, &QAction::triggered, this, &TabSupervisor::actTabReport);
aTabModeration = new QAction(this);
aTabModeration->setCheckable(true);
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts);
refreshShortcuts();
@ -210,6 +225,8 @@ void TabSupervisor::retranslateUi()
aTabReplays->setText(tr("Game Replays"));
aTabAdmin->setText(tr("Administration"));
aTabLog->setText(tr("Logs"));
aTabReport->setText(tr("Report Queue"));
aTabModeration->setText(tr("Moderation"));
// tabs
QList<Tab *> tabs;
@ -219,6 +236,8 @@ void TabSupervisor::retranslateUi()
tabs.append(tabAdmin);
tabs.append(tabAccount);
tabs.append(tabLog);
tabs.append(tabReport);
tabs.append(tabModeration);
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext()) {
tabs.append(roomIterator.next().value());
@ -265,6 +284,8 @@ void TabSupervisor::refreshShortcuts()
aTabReplays->setShortcuts(shortcuts.getShortcut("Tabs/aTabReplays"));
aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin"));
aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog"));
aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport"));
aTabModeration->setShortcuts(shortcuts.getShortcut("Tabs/aTabModeration"));
}
void TabSupervisor::closeEvent(QCloseEvent *event)
@ -317,7 +338,12 @@ static void checkAndTrigger(QAction *checkableAction, bool checked)
}
/**
* Opens the always-available tabs, depending on settings.
* Opens the always-available tabs, depending on settings, and lands on the configured startup tab.
*
* The startup destination is a request: tabs that were not open before (deck editors, storage
* tabs disabled in the Tabs menu) are opened as part of the startup flow. Destinations that
* require a server connection (Server, Server Room) are handled asynchronously by MainWindow
* through the intent system, since this class has no RemoteClient.
*/
void TabSupervisor::initStartupTabs()
{
@ -333,6 +359,42 @@ void TabSupervisor::initStartupTabs()
if (SettingsCache::instance().tabs().getTabReplaysOpen()) {
openTabReplays();
}
switch (SettingsCache::instance().tabs().getStartupTabIndex()) {
case StartupTab::StartupTabVisualDeckStorage:
if (!tabVisualDeckStorage) {
openTabVisualDeckStorage();
}
setCurrentWidget(tabVisualDeckStorage);
break;
case StartupTab::StartupTabDeckStorage:
if (!tabDeckStorage) {
openTabDeckStorage();
}
setCurrentWidget(tabDeckStorage);
break;
case StartupTab::StartupTabReplays:
if (!tabReplays) {
openTabReplays();
}
setCurrentWidget(tabReplays);
break;
case StartupTab::StartupTabDeckEditor:
addDeckEditorTab(LoadedDeck());
break;
case StartupTab::StartupTabVisualDeckEditor:
addVisualDeckEditorTab(LoadedDeck());
break;
case StartupTab::StartupTabServer:
case StartupTab::StartupTabServerRoom:
// Handled asynchronously by MainWindow::applyStartupDestination(); Home stays selected
// until the server connection succeeds.
break;
case StartupTab::StartupTabHome:
default:
setCurrentWidget(tabHome);
break;
}
}
/**
@ -347,6 +409,7 @@ int TabSupervisor::myAddTab(Tab *tab, QAction *manager)
{
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
connect(tab, &TabGame::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
QString tabText = tab->getTabText();
int idx = addTab(tab, sanitizeTabName(tabText));
@ -424,6 +487,8 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
tabsMenu->addAction(aTabAdmin);
tabsMenu->addAction(aTabLog);
tabsMenu->addAction(aTabCardArtRules);
tabsMenu->addAction(aTabReport);
tabsMenu->addAction(aTabModeration);
if (SettingsCache::instance().tabs().getTabAdminOpen()) {
openTabAdmin();
@ -431,6 +496,12 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
if (SettingsCache::instance().tabs().getTabLogOpen()) {
openTabLog();
}
if (SettingsCache::instance().tabs().getTabReportOpen()) {
openTabReport();
}
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
openTabModeration();
}
openTabCardArtRules();
}
@ -444,6 +515,8 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
tabAccount = nullptr;
tabAdmin = nullptr;
tabLog = nullptr;
tabReport = nullptr;
tabModeration = nullptr;
isLocalGame = true;
userInfo = new ServerInfo_User;
localClients = _clients;
@ -485,6 +558,12 @@ void TabSupervisor::stop()
if (tabLog) {
tabLog->close();
}
if (tabReport) {
tabReport->close();
}
if (tabModeration) {
tabModeration->close();
}
}
QList<Tab *> tabsToDelete;
@ -722,6 +801,59 @@ void TabSupervisor::openTabLog()
aTabLog->setChecked(true);
}
void TabSupervisor::actTabReport(bool checked)
{
SettingsCache::instance().tabs().setTabReportOpen(checked);
if (checked && !tabReport) {
openTabReport();
setCurrentWidget(tabReport);
} else if (!checked && tabReport) {
tabReport->closeRequest();
}
}
void TabSupervisor::openTabReport()
{
tabReport = new TabReport(this, client);
myAddTab(tabReport, aTabReport);
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
connect(tabReport, &QObject::destroyed, this, [this] {
tabReport = nullptr;
aTabReport->setChecked(false);
});
aTabReport->setChecked(true);
}
void TabSupervisor::actTabModeration(bool checked)
{
SettingsCache::instance().tabs().setTabModerationOpen(checked);
if (checked && !tabModeration) {
openTabModeration();
setCurrentWidget(tabModeration);
} else if (!checked && tabModeration) {
tabModeration->closeRequest();
}
}
void TabSupervisor::openTabModeration(const QString &userName)
{
if (tabModeration) {
setCurrentWidget(tabModeration);
if (!userName.isEmpty()) {
tabModeration->investigate(userName);
}
return;
}
tabModeration = new TabModeration(this, client, userName);
myAddTab(tabModeration, aTabModeration);
connect(tabModeration, &QObject::destroyed, this, [this] {
tabModeration = nullptr;
aTabModeration->setChecked(false);
});
aTabModeration->setChecked(true);
}
void TabSupervisor::updatePingTime(int value, int max)
{
if (!tabServer) {
@ -734,6 +866,23 @@ void TabSupervisor::updatePingTime(int value, int max)
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
}
void TabSupervisor::updateLatencyTooltip(const LatencyTracker::Stats &stats)
{
if (!tabServer) {
return;
}
if (stats.sampleCount == 0) {
setTabToolTip(indexOf(tabServer), QString());
return;
}
setTabToolTip(indexOf(tabServer),
tr("Connection quality over the last %n sample(s):", "", stats.sampleCount) + "\n" +
tr("Last: %1 ms").arg(stats.lastMs) + "\n" + tr("Median: %1 ms").arg(stats.medianMs) + "\n" +
tr("95th percentile: %1 ms").arg(stats.p95Ms) + "\n" + tr("Maximum: %1 ms").arg(stats.maxMs));
}
void TabSupervisor::gameJoined(const Event_GameJoined &event)
{
QMap<int, QString> roomGameTypes;
@ -792,6 +941,7 @@ void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tab, &TabRoom::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
myAddTab(tab);
roomTabs.insert(info.room_id(), tab);
if (setCurrent) {
@ -838,6 +988,30 @@ void TabSupervisor::replayLeft(TabGame *tab)
replayTabs.removeOne(tab);
}
void TabSupervisor::joinReportGame(const int gameId, const int roomId)
{
auto *remoteClient = qobject_cast<RemoteClient *>(client);
if (!remoteClient) {
actShowPopup(tr("Report joins are only available on a remote server."));
return;
}
auto ctx = std::make_unique<ContextJoinGame>();
ctx->roomContext.serverContext.hostname = remoteClient->peerName();
ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort());
ctx->roomContext.roomId = roomId;
ctx->gameId = gameId;
ctx->asSpectator = true;
auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx));
joinGameIntent->setParent(this);
connect(joinGameIntent, &Intent::failed, this, [gameId](const QString &reason) {
actShowPopup(tr("Could not join game %1.\n%2").arg(gameId).arg(reason));
});
joinGameIntent->execute();
}
TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus)
{
if (receiverName == QString::fromStdString(userInfo->name())) {
@ -845,8 +1019,10 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
}
ServerInfo_User otherUser;
bool userOnline = false;
if (auto user = userListManager->getOnlineUser(receiverName)) {
otherUser = ServerInfo_User(*user);
userOnline = true;
} else {
otherUser.set_name(receiverName.toStdString());
}
@ -860,9 +1036,10 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
return tab;
}
tab = new TabMessage(this, client, *userInfo, otherUser);
tab = new TabMessage(this, client, *userInfo, otherUser, userOnline);
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
connect(tab, &TabMessage::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
myAddTab(tab);
messageTabs.insert(receiverName, tab);
if (focus) {
@ -886,6 +1063,53 @@ void TabSupervisor::talkLeft(TabMessage *tab)
removeTab(indexOf(tab));
}
QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) const
{
QList<GameInviteOption> options;
if (isLocalGame) {
return options;
}
// The inviter may be in several games of the same room (hosting one and
// spectating another, for example). Return every game so the caller can
// let the user choose which one to invite to.
for (TabGame *tab : gameTabs) {
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
if (metaInfo->proto().room_id() != roomId) {
continue;
}
// A closed game is a dead end — drop it. Started/full games stay
// listed: an invite to them is a legitimate "come spectate" offer.
if (metaInfo->proto().closed()) {
continue;
}
const int gameId = metaInfo->gameId();
const QString description = QString::fromStdString(metaInfo->proto().description());
GameInviteOption option{
.gameId = gameId,
.label =
description.isEmpty() ? tr("Game #%1").arg(gameId) : tr("Game #%1 — %2").arg(gameId).arg(description),
.url = makeGameJoinLink(client->serverName(), client->serverPort(), roomId, gameId, description),
.description = description,
.onlyBuddies = metaInfo->proto().only_buddies(),
.creatorName = QString::fromStdString(metaInfo->proto().creator_info().name()),
};
options.append(option);
}
return options;
}
void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText)
{
TabMessage *tab = addMessageTab(userName, true);
if (tab) {
tab->sendInviteMessage(inviteText);
}
}
/**
* Creates a new deck editor tab and loads the deck into it.
* Creates either a classic or visual deck editor tab depending on settings
@ -1172,6 +1396,24 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
}
break;
}
case Event_NotifyUser::REPORT_RESOLVED: {
QString title = QString::fromStdString(event.custom_title()).simplified();
QString content = QString::fromStdString(event.custom_content()).trimmed();
if (!title.isEmpty() && !content.isEmpty()) {
actShowPopup(title + "\n" + content);
QApplication::alert(this);
}
break;
}
case Event_NotifyUser::REPORT_COMMENT: {
QString title = QString::fromStdString(event.custom_title()).simplified();
QString content = QString::fromStdString(event.custom_content()).trimmed();
if (!title.isEmpty() && !content.isEmpty()) {
actShowPopup(title + "\n" + content);
QApplication::alert(this);
}
break;
}
default:;
}
}