From 31878eb8803ad0f7a0679f23f47ec5997ff5d491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 13 Oct 2026 22:08:58 +0200 Subject: [PATCH] [Client] Report queue dashboard and status pills TabReport becomes a dashboard: selected-report identity header (display-size user, bold status, category/time/room subline), Description/Chat/Comments/Reported-user tabbed sections with tab memory, always-visible user context with empty cues, and bold reported users. ReportStatusDelegate paints palette-tinted status pills with paint-through selection, translated status strings, and font/elision handling. report_utils gains formatReportStatus plus an opt-out status-color flag so the delegate owns color in the mod queue while the user dialog keeps its colors. Async responses stay guarded; retranslation re-applies header and comment states. --- cockatrice/CMakeLists.txt | 1 + .../widgets/tabs/report_status_delegate.cpp | 77 +++++++++ .../widgets/tabs/report_status_delegate.h | 22 +++ .../src/interface/widgets/tabs/tab_report.cpp | 160 ++++++++++++++---- .../src/interface/widgets/tabs/tab_report.h | 15 +- .../widgets/utility/report_utils.cpp | 28 ++- .../interface/widgets/utility/report_utils.h | 4 +- 7 files changed, 262 insertions(+), 45 deletions(-) create mode 100644 cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/report_status_delegate.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 57131e8b6..4707c3f32 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -376,6 +376,7 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp + src/interface/widgets/tabs/report_status_delegate.cpp src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab_account.cpp src/interface/widgets/tabs/tab_admin.cpp diff --git a/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp b/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp new file mode 100644 index 000000000..fe59702c4 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp @@ -0,0 +1,77 @@ +#include "report_status_delegate.h" + +#include +#include +#include +#include +#include + +namespace +{ +constexpr qreal PILL_ALPHA = 0.30; +constexpr int PILL_PAD_X = 10; +constexpr int PILL_PAD_Y = 4; +} // namespace + +ReportStatusDelegate::ReportStatusDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +void ReportStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const bool selected = (option.state & QStyle::State_Selected) != 0; + QString status = index.data(Qt::UserRole).toString(); + if (status.isEmpty()) { + status = index.data(Qt::DisplayRole).toString(); + } + + QPalette::ColorRole tintRole = QPalette::NoRole; + if (status == QStringLiteral("open")) { + tintRole = QPalette::Highlight; + } else if (status == QStringLiteral("assigned")) { + tintRole = QPalette::Link; + } else if (status == QStringLiteral("resolved")) { + tintRole = QPalette::PlaceholderText; + } + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + painter->setFont(opt.font); + + if (tintRole == QPalette::NoRole) { + const QPalette::ColorRole textRole = selected ? QPalette::HighlightedText : QPalette::PlaceholderText; + QStyle *fallbackStyle = opt.widget != nullptr ? opt.widget->style() : QApplication::style(); + fallbackStyle->drawItemText(painter, opt.rect, Qt::AlignLeft | Qt::AlignVCenter, opt.palette, true, opt.text, + textRole); + return; + } + + const QRect pillRect = opt.rect.adjusted(PILL_PAD_X / 2, PILL_PAD_Y / 2, -PILL_PAD_X / 2, -PILL_PAD_Y / 2); + if (pillRect.width() <= 0 || pillRect.height() <= 0) { + QStyledItemDelegate::paint(painter, opt, index); + return; + } + + if (!selected) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + QColor pill = opt.palette.color(tintRole); + pill.setAlphaF(PILL_ALPHA); + painter->setPen(Qt::NoPen); + painter->setBrush(pill); + painter->drawRoundedRect(pillRect, pillRect.height() / 2.0, pillRect.height() / 2.0); + painter->restore(); + } + + const QString elided = + opt.fontMetrics.elidedText(opt.text.isEmpty() ? status : opt.text, opt.textElideMode, pillRect.width()); + const QPalette::ColorRole textRole = selected ? QPalette::HighlightedText : QPalette::WindowText; + QStyle *style = opt.widget != nullptr ? opt.widget->style() : QApplication::style(); + style->drawItemText(painter, pillRect, Qt::AlignCenter, opt.palette, true, elided, textRole); +} + +QSize ReportStatusDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const QSize hint = QStyledItemDelegate::sizeHint(option, index); + return hint + QSize(PILL_PAD_X, PILL_PAD_Y); +} diff --git a/cockatrice/src/interface/widgets/tabs/report_status_delegate.h b/cockatrice/src/interface/widgets/tabs/report_status_delegate.h new file mode 100644 index 000000000..5c168bcd6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/report_status_delegate.h @@ -0,0 +1,22 @@ +#ifndef REPORT_STATUS_DELEGATE_H +#define REPORT_STATUS_DELEGATE_H + +#include +#include + +/** + * Status pill for the report queue table. Paints the status text on a + * translucent badge tinted with a palette role per status, so the colors + * always follow the active theme. Falls back to default painting for + * selected rows and unknown statuses. + */ +class ReportStatusDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit ReportStatusDelegate(QObject *parent = nullptr); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +#endif // REPORT_STATUS_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.cpp b/cockatrice/src/interface/widgets/tabs/tab_report.cpp index 988c3e7d9..ea2f47902 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -4,6 +4,7 @@ #include "../utility/report_utils.h" #include "abstract_client.h" #include "libcockatrice/protocol/pb/response.pb.h" +#include "report_status_delegate.h" #include "tab_supervisor.h" #include @@ -22,7 +23,7 @@ #include #include #include -#include +#include #include #include #include @@ -124,25 +125,42 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged); table->setContextMenuPolicy(Qt::CustomContextMenu); connect(table, &QTableWidget::customContextMenuRequested, this, &TabReport::showTableContextMenu); + table->setItemDelegateForColumn(COL_STATUS, new ReportStatusDelegate(table)); userContextMenu = new UserContextMenu(tabSupervisor, this); connect(userContextMenu, &UserContextMenu::openMessageDialog, this, &TabReport::openMessageDialog); - descGroup = new QGroupBox; + reportHeaderName = new QLabel; + QFont displayFont = reportHeaderName->font(); + displayFont.setPointSize(displayFont.pointSize() + 6); + displayFont.setBold(true); + reportHeaderName->setFont(displayFont); + reportHeaderStatus = new QLabel; + QFont statusFont = reportHeaderStatus->font(); + statusFont.setBold(true); + reportHeaderStatus->setFont(statusFont); + reportHeaderSubline = new QLabel; + + auto *reportIdentityRow = new QHBoxLayout; + reportIdentityRow->addWidget(reportHeaderName); + reportIdentityRow->addStretch(); + reportIdentityRow->addWidget(reportHeaderStatus); + + auto *descPage = new QWidget; descriptionEdit = new QTextEdit; descriptionEdit->setReadOnly(true); - auto *descLayout = new QVBoxLayout(descGroup); + auto *descLayout = new QVBoxLayout(descPage); descLayout->setContentsMargins(4, 4, 4, 4); descLayout->addWidget(descriptionEdit); - chatGroup = new QGroupBox; + auto *chatPage = new QWidget; chatLogEdit = new QTextEdit; chatLogEdit->setReadOnly(true); chatLogEdit->setFont(QtUtils::monospaceFont()); - auto *chatLayout = new QVBoxLayout(chatGroup); + auto *chatLayout = new QVBoxLayout(chatPage); chatLayout->setContentsMargins(4, 4, 4, 4); chatLayout->addWidget(chatLogEdit); - commentsGroup = new QGroupBox; + auto *commentsPage = new QWidget; commentsEdit = new QTextEdit; commentsEdit->setReadOnly(true); auto *commentInputRow = new QHBoxLayout; @@ -153,12 +171,12 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T connect(commentInput, &QLineEdit::returnPressed, this, &TabReport::addComment); commentInputRow->addWidget(commentInput); commentInputRow->addWidget(commentButton); - auto *commentsLayout = new QVBoxLayout(commentsGroup); + auto *commentsLayout = new QVBoxLayout(commentsPage); commentsLayout->setContentsMargins(4, 4, 4, 4); commentsLayout->addWidget(commentsEdit); commentsLayout->addLayout(commentInputRow); - userContextGroup = new QGroupBox; + auto *userPage = new QWidget; userContextName = new QLabel; userContextAccountAge = new QLabel; userContextReports = new QLabel; @@ -177,7 +195,7 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T userContextWarnsLabel = new QLabel; userContextNotesLabel = new QLabel; userContextRecentReportsLabel = new QLabel; - auto *ucLayout = new QGridLayout(userContextGroup); + auto *ucLayout = new QGridLayout(userPage); ucLayout->setContentsMargins(8, 8, 8, 8); ucLayout->addWidget(userContextUserLabel, 0, 0); ucLayout->addWidget(userContextName, 0, 1, 1, 3); @@ -193,7 +211,6 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T ucLayout->addWidget(userContextNotes, 3, 1, 1, 3); ucLayout->addWidget(userContextRecentReportsLabel, 4, 0, 1, 4); ucLayout->addWidget(userContextRecentReports, 5, 0, 1, 4); - userContextGroup->setVisible(false); statsGroup = new QGroupBox; statsGroup->setCheckable(true); @@ -220,13 +237,11 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T } }); - detailSplitter = new QSplitter(Qt::Vertical); - detailSplitter->addWidget(descGroup); - detailSplitter->addWidget(chatGroup); - detailSplitter->addWidget(commentsGroup); - detailSplitter->setStretchFactor(0, 1); - detailSplitter->setStretchFactor(1, 1); - detailSplitter->setStretchFactor(2, 2); + detailTabs = new QTabWidget; + detailTabs->addTab(descPage, QString()); + detailTabs->addTab(chatPage, QString()); + detailTabs->addTab(commentsPage, QString()); + detailTabs->addTab(userPage, QString()); assignButton = new QPushButton; resolveButton = new QPushButton; @@ -258,12 +273,14 @@ TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : T layout->addLayout(topBar); layout->addWidget(statsLabel); layout->addWidget(table, 2); - layout->addWidget(detailSplitter, 1); - layout->addWidget(userContextGroup); + layout->addLayout(reportIdentityRow); + layout->addWidget(reportHeaderSubline); + layout->addWidget(detailTabs, 2); layout->addWidget(statsGroup); layout->addLayout(actionBar); retranslateUi(); + clearReportHeader(); setActionsEnabled(false); refreshList(); @@ -285,18 +302,18 @@ void TabReport::retranslateUi() table->setHorizontalHeaderLabels({tr("#"), tr("Time"), tr("Reporter"), tr("Reported User"), tr("Category"), tr("Game ID"), tr("Status"), tr("Assigned To"), tr("Replay"), tr("Room")}); - descGroup->setTitle(tr("Description")); - chatGroup->setTitle(tr("Chat Log Context")); - chatGroup->setToolTip( - tr("Chat log attached by the reporter. It is captured from their client and may be incomplete or edited.")); - commentsGroup->setTitle(tr("Comments / Thread")); + detailTabs->setTabText(0, tr("Description")); + detailTabs->setTabText(1, tr("Chat Log Context")); + detailTabs->setTabToolTip(1, tr("Chat log attached by the reporter. It is captured from their client and may be " + "incomplete or edited.")); + detailTabs->setTabText(2, tr("Comments / Thread")); + detailTabs->setTabText(3, tr("Reported User")); descriptionEdit->setPlaceholderText(tr("No description.")); chatLogEdit->setPlaceholderText(tr("No chat log attached.")); commentsEdit->setPlaceholderText(tr("No comments yet.")); commentInput->setPlaceholderText(tr("Type a reply...")); commentButton->setText(tr("Send")); - userContextGroup->setTitle(tr("Reported User Context")); userContextUserLabel->setText(tr("User:")); userContextAgeLabel->setText(tr("Account Age:")); userContextReportsLabel->setText(tr("Reports:")); @@ -304,10 +321,18 @@ void TabReport::retranslateUi() userContextWarnsLabel->setText(tr("Warns:")); userContextNotesLabel->setText(tr("Admin Notes:")); userContextRecentReportsLabel->setText(tr("Recent Reports Against User:")); - userContextAccountAge->setText(QString()); - userContextReports->setText(QString()); - userContextBans->setText(QString()); - userContextWarns->setText(QString()); + + if (headerReportId >= 0) { + for (const ServerInfo_Report &r : filteredReports) { + if (r.report_id() == headerReportId) { + updateReportHeader(r); + const QString status = QString::fromStdString(r.status()); + const bool canComment = (status == "open" || status == "assigned"); + commentInput->setPlaceholderText(canComment ? tr("Type a reply...") : tr("This report is closed.")); + break; + } + } + } statsGroup->setTitle(tr("Report Statistics")); @@ -400,7 +425,8 @@ void TabReport::reportListResponse(const Response &response) chatLogEdit->clear(); commentsEdit->clear(); setActionsEnabled(false); - userContextGroup->setVisible(false); + clearReportHeader(); + clearUserContext(); } } @@ -441,7 +467,7 @@ void TabReport::applyFilters() const ServerInfo_Report &r = filteredReports[row]; report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID, - COL_STATUS, COL_ASSIGNED); + COL_STATUS, COL_ASSIGNED, false); table->setItem(row, COL_REPORTER, new QTableWidgetItem(QString::fromStdString(r.reporter_name()))); @@ -452,10 +478,32 @@ void TabReport::applyFilters() new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : "")); } + QFont reportedFont = table->font(); + reportedFont.setBold(true); + for (int row = 0; row < filteredReports.size(); ++row) { + if (auto *reportedItem = table->item(row, COL_REPORTED)) { + reportedItem->setFont(reportedFont); + } + } + table->setSortingEnabled(true); table->resizeColumnsToContents(); table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch); + if (headerReportId >= 0) { + bool stillVisible = false; + for (const ServerInfo_Report &r : filteredReports) { + if (r.report_id() == headerReportId) { + stillVisible = true; + break; + } + } + if (!stillVisible) { + clearReportHeader(); + clearUserContext(); + } + } + statusLabel->setText(tr("%1 report(s)").arg(filteredReports.size())); } @@ -482,6 +530,31 @@ void TabReport::updateStats() .arg(dismissed)); } +void TabReport::updateReportHeader(const ServerInfo_Report &report) +{ + reportHeaderName->setText(QString::fromStdString(report.reported_user_name())); + reportHeaderStatus->setText(report_utils::formatReportStatus(QString::fromStdString(report.status()))); + + QStringList parts; + parts << QString::fromStdString(report.category()); + parts << QDateTime::fromSecsSinceEpoch(report.report_time()).toLocalTime().toString("yyyy-MM-dd HH:mm"); + if (report.has_room_id() && report.room_id() > 0) { + parts << tr("Room %1").arg(report.room_id()); + } + if (report.game_id() > 0) { + parts << tr("Game %1").arg(report.game_id()); + } + reportHeaderSubline->setText(parts.join(QStringLiteral(" · "))); +} + +void TabReport::clearReportHeader() +{ + headerReportId = -1; + reportHeaderName->setText(tr("Select a report to inspect.")); + reportHeaderStatus->clear(); + reportHeaderSubline->clear(); +} + void TabReport::onSelectionChanged() { const int row = table->currentRow(); @@ -492,7 +565,8 @@ void TabReport::onSelectionChanged() commentInput->clear(); commentButton->setEnabled(false); setActionsEnabled(false); - userContextGroup->setVisible(false); + clearReportHeader(); + clearUserContext(); return; } @@ -501,6 +575,18 @@ void TabReport::onSelectionChanged() updateActionStates(); } +void TabReport::clearUserContext() +{ + lastRequestedUser.clear(); + userContextName->setText(tr("—")); + userContextAccountAge->clear(); + userContextReports->clear(); + userContextBans->clear(); + userContextWarns->clear(); + userContextNotes->setPlainText(tr("Select a report to inspect.")); + userContextRecentReports->clear(); +} + void TabReport::showTableContextMenu(const QPoint &pos) { QTableWidgetItem *clickedItem = table->itemAt(pos); @@ -538,12 +624,17 @@ void TabReport::loadReportDetails(int reportId) for (const ServerInfo_Report &r : filteredReports) { if (r.report_id() == reportId) { descriptionEdit->setPlainText(QString::fromStdString(r.description())); + updateReportHeader(r); + if (headerReportId != reportId) { + headerReportId = reportId; + detailTabs->setCurrentIndex(0); + } QString reportedUser = QString::fromStdString(r.reported_user_name()); if (!reportedUser.isEmpty()) { requestUserInfo(reportedUser); } else { - userContextGroup->setVisible(false); + clearUserContext(); } QString status = QString::fromStdString(r.status()); @@ -863,12 +954,11 @@ void TabReport::addCommentResponse(const Response &response) void TabReport::requestUserInfo(const QString &userName) { if (userName.isEmpty()) { - userContextGroup->setVisible(false); + clearUserContext(); return; } lastRequestedUser = userName; - userContextGroup->setVisible(true); userContextName->setText(userName); userContextAccountAge->setText(tr("Loading...")); userContextReports->clear(); diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.h b/cockatrice/src/interface/widgets/tabs/tab_report.h index 32ebe1a3f..8607883ce 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.h +++ b/cockatrice/src/interface/widgets/tabs/tab_report.h @@ -16,8 +16,8 @@ class QGroupBox; class QLabel; class QLineEdit; class QPushButton; -class QSplitter; class QTableWidget; +class QTabWidget; class QTextEdit; class QTimer; class GameReplay; @@ -71,6 +71,9 @@ private: void applyFilters(); void updateStats(); void loadReportDetails(int reportId); + void updateReportHeader(const ServerInfo_Report &report); + void clearReportHeader(); + void clearUserContext(); AbstractClient *client; @@ -80,10 +83,10 @@ private: QPushButton *refreshButton; QLabel *statsLabel; QTableWidget *table; - QSplitter *detailSplitter; - QGroupBox *descGroup; - QGroupBox *chatGroup; - QGroupBox *commentsGroup; + QLabel *reportHeaderName; + QLabel *reportHeaderStatus; + QLabel *reportHeaderSubline; + QTabWidget *detailTabs; QTextEdit *descriptionEdit; QTextEdit *chatLogEdit; QTextEdit *commentsEdit; @@ -99,7 +102,6 @@ private: QTimer *refreshTimer; UserContextMenu *userContextMenu; - QGroupBox *userContextGroup; QLabel *userContextName; QLabel *userContextAccountAge; QLabel *userContextReports; @@ -124,6 +126,7 @@ private: QList allReports; QList filteredReports; + int headerReportId = -1; int detailsRequestedReportId = -1; int detailsRequestSeq = 0; int selectedReportIdBeforeRefresh = -1; diff --git a/cockatrice/src/interface/widgets/utility/report_utils.cpp b/cockatrice/src/interface/widgets/utility/report_utils.cpp index d7fc083c6..f10a7d7d1 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.cpp +++ b/cockatrice/src/interface/widgets/utility/report_utils.cpp @@ -64,6 +64,23 @@ QString formatReportTime(qint64 secondsSinceEpoch) return dt.toString("yyyy-MM-dd hh:mm"); } +QString formatReportStatus(const QString &status) +{ + if (status == QStringLiteral("open")) { + return QObject::tr("Open"); + } + if (status == QStringLiteral("assigned")) { + return QObject::tr("Assigned"); + } + if (status == QStringLiteral("resolved")) { + return QObject::tr("Resolved"); + } + if (status == QStringLiteral("dismissed")) { + return QObject::tr("Dismissed"); + } + return status; +} + void fillReportTableRow(QTableWidget *table, int row, const ServerInfo_Report &report, @@ -73,7 +90,8 @@ void fillReportTableRow(QTableWidget *table, int categoryColumn, int gameIdColumn, int statusColumn, - int assignedColumn) + int assignedColumn, + bool colorizeStatus) { auto *idItem = new QTableWidgetItem(QString::number(report.report_id())); idItem->setData(Qt::UserRole, report.report_id()); @@ -86,8 +104,12 @@ void fillReportTableRow(QTableWidget *table, table->setItem(row, gameIdColumn, new QTableWidgetItem(report.game_id() > 0 ? QString::number(report.game_id()) : QString())); - auto *statusItem = new QTableWidgetItem(QString::fromStdString(report.status())); - setReportStatusItemColor(statusItem, QString::fromStdString(report.status())); + const QString rawStatus = QString::fromStdString(report.status()); + auto *statusItem = new QTableWidgetItem(formatReportStatus(rawStatus)); + statusItem->setData(Qt::UserRole, rawStatus); + if (colorizeStatus) { + setReportStatusItemColor(statusItem, rawStatus); + } table->setItem(row, statusColumn, statusItem); table->setItem(row, assignedColumn, new QTableWidgetItem(QString::fromStdString(report.assigned_mod_name()))); diff --git a/cockatrice/src/interface/widgets/utility/report_utils.h b/cockatrice/src/interface/widgets/utility/report_utils.h index 679989486..e22dbd980 100644 --- a/cockatrice/src/interface/widgets/utility/report_utils.h +++ b/cockatrice/src/interface/widgets/utility/report_utils.h @@ -13,6 +13,7 @@ namespace report_utils QString formatReportCategory(const QString &category); QString formatReportTime(qint64 secondsSinceEpoch); +QString formatReportStatus(const QString &status); void fillReportTableRow(QTableWidget *table, int row, @@ -23,7 +24,8 @@ void fillReportTableRow(QTableWidget *table, int categoryColumn, int gameIdColumn, int statusColumn, - int assignedColumn); + int assignedColumn, + bool colorizeStatus = true); void renderReportDetails(QTextEdit *chatLogEdit, QTextEdit *commentsEdit,