mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
[Client] Report queue dashboard and status pills (#7515)
* [Client] Moderation and alts dashboards Turn the two staff tables into dashboards without behavior changes: TabModeration gets an identity header (display-size name, status, registered/last-login subline), Reports/Warnings/Bans/Alts count cards with an Open-graph deep link, full-width admin notes, and Alts/Sessions/Staff tabbed sections with tab memory. TabAlts gets toggle chips with match counts, bold usernames, palette-token dimming of shared-IP rows, and a resizable side card for the selected-user timeline with selection restore. Row activation works via Enter as well as double-click; async responses are guarded against stale investigations and retranslation re-applies cached dashboard data. * [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. * Fix remaining review threads on the report queue dashboard - Read the raw report status from Qt::UserRole in DlgMyReports, so regular users can comment on their own open reports again. - Exclude the server's depth-0 seed row from the related-alt count. - Dim only IP matches shared by more than the pair, not every IP match. - Activate alts rows on double-click or Enter instead of `activated`, which fires on a single click on single-click platforms. - Fold the reported-user bolding pass into the fill loop. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
e603257e08
commit
18f9b26a3d
14 changed files with 615 additions and 171 deletions
|
|
@ -295,6 +295,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/utility/get_text_with_max.cpp
|
||||
src/interface/widgets/utility/report_utils.cpp
|
||||
src/interface/widgets/utility/report_utils.h
|
||||
src/interface/widgets/utility/table_activation.cpp
|
||||
src/interface/widgets/utility/table_activation.h
|
||||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.h
|
||||
|
|
@ -382,6 +384,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
|
||||
|
|
|
|||
|
|
@ -230,7 +230,8 @@ void DlgMyReports::onSelectionChanged()
|
|||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportDetailsResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
QString status = table->item(row, COL_STATUS)->text();
|
||||
// The cell text is the translated label; the raw status lives in UserRole.
|
||||
const QString status = table->item(row, COL_STATUS)->data(Qt::UserRole).toString();
|
||||
bool canComment = (status == "open" || status == "assigned");
|
||||
commentButton->setEnabled(canComment);
|
||||
commentInput->setEnabled(canComment);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
#include "report_status_delegate.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QStyle>
|
||||
#include <QStyleOptionViewItem>
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
#ifndef REPORT_STATUS_DELEGATE_H
|
||||
#define REPORT_STATUS_DELEGATE_H
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -1,22 +1,25 @@
|
|||
#include "tab_alts.h"
|
||||
|
||||
#include "../utility/epoch_format.h"
|
||||
#include "../utility/table_activation.h"
|
||||
#include "abstract_client.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
#include "libcockatrice/protocol/pb/response_user_alts.pb.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QCheckBox>
|
||||
#include <QDateTime>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextEdit>
|
||||
|
|
@ -47,6 +50,17 @@ constexpr int COL_ALTS_BANS = 9;
|
|||
constexpr int COL_ALTS_ACTIVE = 10;
|
||||
constexpr int COL_ALTS_COUNT = 11;
|
||||
|
||||
constexpr int COL_WARNS_TIME = 0;
|
||||
constexpr int COL_WARNS_MODERATOR = 1;
|
||||
constexpr int COL_WARNS_REASON = 2;
|
||||
constexpr int COL_WARNS_COUNT = 3;
|
||||
|
||||
constexpr int COL_BANS_TIME = 0;
|
||||
constexpr int COL_BANS_MODERATOR = 1;
|
||||
constexpr int COL_BANS_LENGTH = 2;
|
||||
constexpr int COL_BANS_REASON = 3;
|
||||
constexpr int COL_BANS_COUNT = 4;
|
||||
|
||||
constexpr int MAX_ALT_DEPTH = 3;
|
||||
// Starting default only (adjustable 1-100000): shared VPN exits routinely
|
||||
// cover dozens of users, so 10 hides the obvious VPN noise but keeps
|
||||
|
|
@ -95,15 +109,16 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
|
|||
topBar->addWidget(depthSpin);
|
||||
topBar->addWidget(investigateButton);
|
||||
|
||||
emailCheck = new QCheckBox;
|
||||
emailCheck->setChecked(true);
|
||||
connect(emailCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
clientIdCheck = new QCheckBox;
|
||||
clientIdCheck->setChecked(true);
|
||||
connect(clientIdCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
ipCheck = new QCheckBox;
|
||||
ipCheck->setChecked(true);
|
||||
connect(ipCheck, &QCheckBox::toggled, this, &TabAlts::onFilterChanged);
|
||||
auto makeChip = [this](const QString &text) {
|
||||
auto *chip = new QPushButton(text);
|
||||
chip->setCheckable(true);
|
||||
chip->setChecked(true);
|
||||
connect(chip, &QPushButton::toggled, this, &TabAlts::onFilterChanged);
|
||||
return chip;
|
||||
};
|
||||
emailChip = makeChip(QString());
|
||||
clientIdChip = makeChip(QString());
|
||||
ipChip = makeChip(QString());
|
||||
vpnLabel = new QLabel;
|
||||
vpnSpin = new QSpinBox;
|
||||
vpnSpin->setMinimum(1);
|
||||
|
|
@ -112,17 +127,20 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
|
|||
connect(vpnSpin, &QSpinBox::valueChanged, this, &TabAlts::onFilterChanged);
|
||||
|
||||
auto *filterBar = new QHBoxLayout;
|
||||
filterBar->addWidget(emailCheck);
|
||||
filterBar->addWidget(clientIdCheck);
|
||||
filterBar->addWidget(ipCheck);
|
||||
filterBar->addWidget(emailChip);
|
||||
filterBar->addWidget(clientIdChip);
|
||||
filterBar->addWidget(ipChip);
|
||||
filterBar->addWidget(vpnLabel);
|
||||
filterBar->addWidget(vpnSpin);
|
||||
filterBar->addStretch();
|
||||
|
||||
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
|
||||
configureAltTable(altsTable);
|
||||
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
|
||||
auto *userItem = altsTable->item(row, COL_ALTS_USER);
|
||||
connectTableActivation(altsTable, this, [this](const QModelIndex &index) {
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
auto *userItem = altsTable->item(index.row(), COL_ALTS_USER);
|
||||
if (userItem != nullptr && !userItem->text().isEmpty() && userItem->text() != currentUser) {
|
||||
investigate(userItem->text());
|
||||
}
|
||||
|
|
@ -135,9 +153,9 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
|
|||
detailNotesEdit->setReadOnly(true);
|
||||
// Compact cap so the warn/ban tables stay visible without scrolling the tab.
|
||||
detailNotesEdit->setMaximumHeight(60);
|
||||
warnsTable = new QTableWidget(0, 3);
|
||||
warnsTable = new QTableWidget(0, COL_WARNS_COUNT);
|
||||
configureAltTable(warnsTable);
|
||||
bansTable = new QTableWidget(0, 4);
|
||||
bansTable = new QTableWidget(0, COL_BANS_COUNT);
|
||||
configureAltTable(bansTable);
|
||||
|
||||
auto *detailGrid = new QGridLayout;
|
||||
|
|
@ -151,21 +169,24 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q
|
|||
detailGrid->addWidget(detailNotesLabel, 1, 0);
|
||||
detailGrid->addWidget(detailNotesEdit, 1, 1, 1, 3);
|
||||
|
||||
auto *detailTables = new QHBoxLayout;
|
||||
detailTables->addWidget(warnsTable);
|
||||
detailTables->addWidget(bansTable);
|
||||
|
||||
detailsGroup = new QGroupBox;
|
||||
auto *detailsLayout = new QVBoxLayout(detailsGroup);
|
||||
detailsLayout->addLayout(detailGrid);
|
||||
detailsLayout->addLayout(detailTables);
|
||||
detailsLayout->addWidget(warnsTable, 1);
|
||||
detailsLayout->addWidget(bansTable, 1);
|
||||
|
||||
auto *contentSplitter = new QSplitter(Qt::Horizontal);
|
||||
contentSplitter->addWidget(altsTable);
|
||||
contentSplitter->addWidget(detailsGroup);
|
||||
contentSplitter->setStretchFactor(0, 3);
|
||||
contentSplitter->setStretchFactor(1, 2);
|
||||
detailsGroup->setMinimumWidth(260);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addLayout(topBar);
|
||||
mainLayout->addLayout(filterBar);
|
||||
mainLayout->addWidget(statusLabel);
|
||||
mainLayout->addWidget(altsTable, 3);
|
||||
mainLayout->addWidget(detailsGroup, 2);
|
||||
mainLayout->addWidget(contentSplitter, 1);
|
||||
|
||||
retranslateUi();
|
||||
clearDetails();
|
||||
|
|
@ -184,15 +205,19 @@ void TabAlts::retranslateUi()
|
|||
statusLabel->setText(tr("Enter a user name to investigate."));
|
||||
}
|
||||
|
||||
emailCheck->setText(tr("Email"));
|
||||
clientIdCheck->setText(tr("Client ID"));
|
||||
ipCheck->setText(tr("IP"));
|
||||
emailChip->setText(tr("Email"));
|
||||
emailChip->setToolTip(tr("Show or hide email matches"));
|
||||
clientIdChip->setText(tr("Client ID"));
|
||||
clientIdChip->setToolTip(tr("Show or hide client ID matches"));
|
||||
ipChip->setText(tr("IP"));
|
||||
ipChip->setToolTip(tr("Show or hide IP address matches"));
|
||||
vpnLabel->setText(tr("Hide IPs shared by more than"));
|
||||
vpnSpin->setSuffix(tr(" users"));
|
||||
|
||||
altsTable->setHorizontalHeaderLabels({tr("User"), tr("Via"), tr("Depth"), tr("Shared"), tr("eMail"),
|
||||
tr("Client ID"), tr("Registered"), tr("Last login"), tr("Warns"), tr("Bans"),
|
||||
tr("Active")});
|
||||
altsTable->setToolTip(tr("Double-click or press Enter on a row to investigate that account"));
|
||||
warnsTable->setHorizontalHeaderLabels({tr("Warning time"), tr("Moderator"), tr("Reason")});
|
||||
bansTable->setHorizontalHeaderLabels({tr("Ban time"), tr("Moderator"), tr("Length"), tr("Reason")});
|
||||
|
||||
|
|
@ -200,6 +225,10 @@ void TabAlts::retranslateUi()
|
|||
detailNameLabel->setText(tr("Name:"));
|
||||
detailCountsLabel->setText(tr("Counts:"));
|
||||
detailNotesLabel->setText(tr("Admin notes:"));
|
||||
|
||||
if (!lastAlts.isEmpty()) {
|
||||
renderTable();
|
||||
}
|
||||
}
|
||||
|
||||
QString TabAlts::formatEpoch(quint64 ts) const
|
||||
|
|
@ -297,13 +326,13 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const
|
|||
}
|
||||
const QString reason = QString::fromStdString(alt.match_reason());
|
||||
if (reason == QStringLiteral("email")) {
|
||||
return emailCheck->isChecked();
|
||||
return emailChip->isChecked();
|
||||
}
|
||||
if (reason.startsWith(QStringLiteral("clientid:"))) {
|
||||
return clientIdCheck->isChecked();
|
||||
return clientIdChip->isChecked();
|
||||
}
|
||||
if (reason.startsWith(QStringLiteral("ip:"))) {
|
||||
return ipCheck->isChecked() &&
|
||||
return ipChip->isChecked() &&
|
||||
(!alt.has_shared_count() || alt.shared_count() <= static_cast<unsigned>(vpnSpin->value()));
|
||||
}
|
||||
return true;
|
||||
|
|
@ -311,7 +340,32 @@ bool TabAlts::rowVisible(const ServerInfo_UserAlt &alt) const
|
|||
|
||||
void TabAlts::renderTable()
|
||||
{
|
||||
// Blocked so the programmatic refill neither wipes the details panel
|
||||
// (via the selection-cleared path) nor refetches them on restore.
|
||||
const QSignalBlocker blocker(altsTable);
|
||||
altsTable->setRowCount(0);
|
||||
|
||||
int emailCount = 0;
|
||||
int clientIdCount = 0;
|
||||
int ipCount = 0;
|
||||
for (const ServerInfo_UserAlt &alt : lastAlts) {
|
||||
const QString reason = QString::fromStdString(alt.match_reason());
|
||||
if (reason == QStringLiteral("email")) {
|
||||
++emailCount;
|
||||
} else if (reason.startsWith(QStringLiteral("clientid:"))) {
|
||||
++clientIdCount;
|
||||
} else if (reason.startsWith(QStringLiteral("ip:"))) {
|
||||
++ipCount;
|
||||
}
|
||||
}
|
||||
emailChip->setText(tr("Email (%1)").arg(emailCount));
|
||||
clientIdChip->setText(tr("Client ID (%1)").arg(clientIdCount));
|
||||
ipChip->setText(tr("IP (%1)").arg(ipCount));
|
||||
|
||||
QFont userFont = altsTable->font();
|
||||
userFont.setBold(true);
|
||||
const QColor dimColor = palette().color(QPalette::Disabled, QPalette::Text);
|
||||
|
||||
int hidden = 0;
|
||||
int row = 0;
|
||||
for (const ServerInfo_UserAlt &alt : lastAlts) {
|
||||
|
|
@ -320,7 +374,9 @@ void TabAlts::renderTable()
|
|||
continue;
|
||||
}
|
||||
altsTable->insertRow(row);
|
||||
altsTable->setItem(row, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
|
||||
auto *userItem = new QTableWidgetItem(QString::fromStdString(alt.user_name()));
|
||||
userItem->setFont(userFont);
|
||||
altsTable->setItem(row, COL_ALTS_USER, userItem);
|
||||
altsTable->setItem(row, COL_ALTS_VIA,
|
||||
new QTableWidgetItem(alt.has_match_reason() && !alt.match_reason().empty()
|
||||
? QString::fromStdString(alt.match_reason())
|
||||
|
|
@ -337,6 +393,14 @@ void TabAlts::renderTable()
|
|||
altsTable->setItem(row, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
|
||||
altsTable->setItem(row, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
|
||||
altsTable->setItem(row, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
|
||||
if (QString::fromStdString(alt.match_reason()).startsWith(QStringLiteral("ip:")) && alt.has_shared_count() &&
|
||||
alt.shared_count() > 2) {
|
||||
for (int col = 0; col < COL_ALTS_COUNT; ++col) {
|
||||
if (auto *item = altsTable->item(row, col)) {
|
||||
item->setForeground(dimColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
++row;
|
||||
}
|
||||
// The server appends the frontier user as a depth-0 seed row; it is the
|
||||
|
|
@ -356,6 +420,22 @@ void TabAlts::renderTable()
|
|||
.arg(relatedCount)
|
||||
.arg(currentUser));
|
||||
}
|
||||
|
||||
if (!selectedUser.isEmpty()) {
|
||||
bool restored = false;
|
||||
for (int restoreRow = 0; restoreRow < altsTable->rowCount(); ++restoreRow) {
|
||||
auto *userItem = altsTable->item(restoreRow, COL_ALTS_USER);
|
||||
if (userItem != nullptr && userItem->text() == selectedUser) {
|
||||
altsTable->setCurrentItem(userItem, QItemSelectionModel::Select | QItemSelectionModel::Rows);
|
||||
restored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!restored) {
|
||||
selectedUser.clear();
|
||||
clearDetails();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabAlts::onFilterChanged()
|
||||
|
|
@ -424,7 +504,7 @@ void TabAlts::requestDetails(const QString &userName)
|
|||
void TabAlts::clearDetails()
|
||||
{
|
||||
detailNameValue->clear();
|
||||
detailCountsValue->clear();
|
||||
detailCountsValue->setText(tr("Select a row to inspect its history."));
|
||||
detailNotesEdit->clear();
|
||||
warnsTable->setRowCount(0);
|
||||
bansTable->setRowCount(0);
|
||||
|
|
@ -461,9 +541,9 @@ void TabAlts::warnHistoryResponse(const Response &response)
|
|||
warnsTable->setRowCount(resp.warn_list_size());
|
||||
for (int i = 0; i < resp.warn_list_size(); ++i) {
|
||||
const ServerInfo_Warning &warn = resp.warn_list(i);
|
||||
warnsTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
|
||||
warnsTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
|
||||
warnsTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(warn.reason())));
|
||||
warnsTable->setItem(i, COL_WARNS_TIME, new QTableWidgetItem(QString::fromStdString(warn.time_of())));
|
||||
warnsTable->setItem(i, COL_WARNS_MODERATOR, new QTableWidgetItem(QString::fromStdString(warn.admin_name())));
|
||||
warnsTable->setItem(i, COL_WARNS_REASON, new QTableWidgetItem(QString::fromStdString(warn.reason())));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -478,9 +558,9 @@ void TabAlts::banHistoryResponse(const Response &response)
|
|||
bansTable->setRowCount(resp.ban_list_size());
|
||||
for (int i = 0; i < resp.ban_list_size(); ++i) {
|
||||
const ServerInfo_Ban &ban = resp.ban_list(i);
|
||||
bansTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
|
||||
bansTable->setItem(i, 1, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
|
||||
bansTable->setItem(i, 2, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
|
||||
bansTable->setItem(i, 3, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
|
||||
bansTable->setItem(i, COL_BANS_TIME, new QTableWidgetItem(QString::fromStdString(ban.ban_time())));
|
||||
bansTable->setItem(i, COL_BANS_MODERATOR, new QTableWidgetItem(QString::fromStdString(ban.admin_name())));
|
||||
bansTable->setItem(i, COL_BANS_LENGTH, new QTableWidgetItem(QString::fromStdString(ban.ban_length())));
|
||||
bansTable->setItem(i, COL_BANS_REASON, new QTableWidgetItem(QString::fromStdString(ban.ban_reason())));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
#include <qtmetamacros.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QCheckBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
|
|
@ -70,9 +70,9 @@ private:
|
|||
QLabel *statusLabel;
|
||||
QTableWidget *altsTable;
|
||||
|
||||
QCheckBox *emailCheck;
|
||||
QCheckBox *clientIdCheck;
|
||||
QCheckBox *ipCheck;
|
||||
QPushButton *emailChip;
|
||||
QPushButton *clientIdChip;
|
||||
QPushButton *ipChip;
|
||||
QLabel *vpnLabel;
|
||||
QSpinBox *vpnSpin;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "tab_moderation.h"
|
||||
|
||||
#include "../utility/epoch_format.h"
|
||||
#include "../utility/table_activation.h"
|
||||
#include "abstract_client.h"
|
||||
#include "libcockatrice/protocol/pb/admin_commands.pb.h"
|
||||
#include "libcockatrice/protocol/pb/response.pb.h"
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
#include <QSharedPointer>
|
||||
#include <QSplitter>
|
||||
#include <QStringList>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextEdit>
|
||||
|
|
@ -101,38 +103,67 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
|
|||
topBar->addWidget(removeBannerButton);
|
||||
|
||||
userInfoGroup = new QGroupBox;
|
||||
userInfoNameLabel = new QLabel;
|
||||
userInfoNameValue = new QLabel;
|
||||
userInfoRegisteredLabel = new QLabel;
|
||||
userInfoRegisteredValue = new QLabel;
|
||||
userInfoLastLoginLabel = new QLabel;
|
||||
userInfoLastLoginValue = new QLabel;
|
||||
userInfoStatusLabel = new QLabel;
|
||||
QFont displayFont = userInfoNameValue->font();
|
||||
displayFont.setPointSize(displayFont.pointSize() + 6);
|
||||
displayFont.setBold(true);
|
||||
userInfoNameValue->setFont(displayFont);
|
||||
userInfoStatusValue = new QLabel;
|
||||
userInfoCountsLabel = new QLabel;
|
||||
userInfoCountsValue = new QLabel;
|
||||
QFont statusFont = userInfoStatusValue->font();
|
||||
statusFont.setBold(true);
|
||||
userInfoStatusValue->setFont(statusFont);
|
||||
sublineLabel = new QLabel;
|
||||
|
||||
auto makeCountCard = [](const QString &title, QLabel *&value) {
|
||||
auto *card = new QGroupBox(title);
|
||||
value = new QLabel(QStringLiteral("–"), card);
|
||||
QFont cardFont = value->font();
|
||||
cardFont.setPointSize(cardFont.pointSize() + 6);
|
||||
cardFont.setBold(true);
|
||||
value->setFont(cardFont);
|
||||
value->setAlignment(Qt::AlignCenter);
|
||||
auto *cardLayout = new QVBoxLayout(card);
|
||||
cardLayout->addWidget(value);
|
||||
return card;
|
||||
};
|
||||
cardReports = makeCountCard(QString(), cardReportsValue);
|
||||
cardWarns = makeCountCard(QString(), cardWarnsValue);
|
||||
cardBans = makeCountCard(QString(), cardBansValue);
|
||||
|
||||
altsCard = new QGroupBox;
|
||||
altsCountValue = new QLabel(QStringLiteral("–"), altsCard);
|
||||
QFont altsFont = altsCountValue->font();
|
||||
altsFont.setPointSize(altsFont.pointSize() + 6);
|
||||
altsFont.setBold(true);
|
||||
altsCountValue->setFont(altsFont);
|
||||
altsCountValue->setAlignment(Qt::AlignCenter);
|
||||
openAltsButton = new QPushButton;
|
||||
connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph);
|
||||
auto *altsCardLayout = new QVBoxLayout(altsCard);
|
||||
altsCardLayout->addWidget(altsCountValue);
|
||||
altsCardLayout->addWidget(openAltsButton);
|
||||
|
||||
auto *cardsRow = new QHBoxLayout;
|
||||
cardsRow->addWidget(cardReports);
|
||||
cardsRow->addWidget(cardWarns);
|
||||
cardsRow->addWidget(cardBans);
|
||||
cardsRow->addWidget(altsCard);
|
||||
|
||||
userInfoNotesLabel = new QLabel;
|
||||
userInfoNotesEdit = new QTextEdit;
|
||||
userInfoNotesEdit->setReadOnly(true);
|
||||
|
||||
auto *infoGrid = new QGridLayout;
|
||||
infoGrid->addWidget(userInfoNameLabel, 0, 0);
|
||||
infoGrid->addWidget(userInfoNameValue, 0, 1);
|
||||
infoGrid->addWidget(userInfoRegisteredLabel, 0, 2);
|
||||
infoGrid->addWidget(userInfoRegisteredValue, 0, 3);
|
||||
infoGrid->addWidget(userInfoLastLoginLabel, 1, 0);
|
||||
infoGrid->addWidget(userInfoLastLoginValue, 1, 1);
|
||||
infoGrid->addWidget(userInfoStatusLabel, 1, 2);
|
||||
infoGrid->addWidget(userInfoStatusValue, 1, 3);
|
||||
infoGrid->addWidget(userInfoCountsLabel, 2, 0);
|
||||
infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3);
|
||||
infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop);
|
||||
infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3);
|
||||
infoGrid->setColumnStretch(1, 1);
|
||||
infoGrid->setColumnStretch(3, 1);
|
||||
auto *identityRow = new QHBoxLayout;
|
||||
identityRow->addWidget(userInfoNameValue);
|
||||
identityRow->addStretch();
|
||||
identityRow->addWidget(userInfoStatusValue);
|
||||
|
||||
auto *infoLayout = new QVBoxLayout(userInfoGroup);
|
||||
infoLayout->addLayout(infoGrid);
|
||||
infoLayout->addLayout(identityRow);
|
||||
infoLayout->addWidget(sublineLabel);
|
||||
infoLayout->addLayout(cardsRow);
|
||||
infoLayout->addWidget(userInfoNotesLabel);
|
||||
infoLayout->addWidget(userInfoNotesEdit);
|
||||
|
||||
auto configureTable = [](QTableWidget *table) {
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
|
|
@ -144,31 +175,27 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
|
|||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
};
|
||||
|
||||
altsGroup = new QGroupBox;
|
||||
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
|
||||
configureTable(altsTable);
|
||||
connect(altsTable, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
|
||||
auto *userItem = altsTable->item(row, COL_ALTS_USER);
|
||||
connectTableActivation(altsTable, this, [this](const QModelIndex &index) {
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
auto *userItem = altsTable->item(index.row(), COL_ALTS_USER);
|
||||
if (userItem != nullptr && !userItem->text().isEmpty()) {
|
||||
getTabSupervisor()->openTabAlts(userItem->text());
|
||||
}
|
||||
});
|
||||
openAltsButton = new QPushButton;
|
||||
connect(openAltsButton, &QPushButton::clicked, this, &TabModeration::openAltsGraph);
|
||||
auto *altsHeader = new QHBoxLayout;
|
||||
altsHeader->addStretch();
|
||||
altsHeader->addWidget(openAltsButton);
|
||||
auto *altsLayout = new QVBoxLayout(altsGroup);
|
||||
altsLayout->addLayout(altsHeader);
|
||||
auto *altsPage = new QWidget;
|
||||
auto *altsLayout = new QVBoxLayout(altsPage);
|
||||
altsLayout->addWidget(altsTable);
|
||||
|
||||
sessionsGroup = new QGroupBox;
|
||||
sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT);
|
||||
configureTable(sessionsTable);
|
||||
auto *sessionsLayout = new QVBoxLayout(sessionsGroup);
|
||||
auto *sessionsPage = new QWidget;
|
||||
auto *sessionsLayout = new QVBoxLayout(sessionsPage);
|
||||
sessionsLayout->addWidget(sessionsTable);
|
||||
|
||||
staffGroup = new QGroupBox;
|
||||
staffTable = new QTableWidget(0, COL_STAFF_COUNT);
|
||||
configureTable(staffTable);
|
||||
refreshStaffButton = new QPushButton;
|
||||
|
|
@ -176,19 +203,21 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli
|
|||
auto *staffHeader = new QHBoxLayout;
|
||||
staffHeader->addStretch();
|
||||
staffHeader->addWidget(refreshStaffButton);
|
||||
auto *staffLayout = new QVBoxLayout(staffGroup);
|
||||
auto *staffPage = new QWidget;
|
||||
auto *staffLayout = new QVBoxLayout(staffPage);
|
||||
staffLayout->addWidget(staffTable);
|
||||
staffLayout->addLayout(staffHeader);
|
||||
|
||||
sectionTabs = new QTabWidget;
|
||||
sectionTabs->addTab(altsPage, QString());
|
||||
sectionTabs->addTab(sessionsPage, QString());
|
||||
sectionTabs->addTab(staffPage, QString());
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Vertical);
|
||||
splitter->addWidget(userInfoGroup);
|
||||
splitter->addWidget(altsGroup);
|
||||
splitter->addWidget(sessionsGroup);
|
||||
splitter->addWidget(staffGroup);
|
||||
splitter->addWidget(sectionTabs);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 2);
|
||||
splitter->setStretchFactor(2, 2);
|
||||
splitter->setStretchFactor(3, 1);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addLayout(topBar);
|
||||
|
|
@ -210,21 +239,28 @@ void TabModeration::retranslateUi()
|
|||
refreshStaffButton->setText(tr("Refresh"));
|
||||
|
||||
userInfoGroup->setTitle(tr("User Info"));
|
||||
userInfoNameLabel->setText(tr("Name:"));
|
||||
userInfoRegisteredLabel->setText(tr("Registered:"));
|
||||
userInfoLastLoginLabel->setText(tr("Last login:"));
|
||||
userInfoStatusLabel->setText(tr("Status:"));
|
||||
userInfoCountsLabel->setText(tr("Counts:"));
|
||||
userInfoNotesLabel->setText(tr("Admin notes:"));
|
||||
|
||||
altsGroup->setTitle(tr("Alts"));
|
||||
openAltsButton->setText(tr("Open alt graph"));
|
||||
cardReports->setTitle(tr("Reports"));
|
||||
cardWarns->setTitle(tr("Warnings"));
|
||||
cardBans->setTitle(tr("Bans"));
|
||||
altsCard->setTitle(tr("Alts"));
|
||||
openAltsButton->setText(tr("Open graph"));
|
||||
openAltsButton->setToolTip(tr("Open this user in the User Alts tab"));
|
||||
sessionsGroup->setTitle(tr("Sessions"));
|
||||
staffGroup->setTitle(tr("Staff Last Logins"));
|
||||
|
||||
sectionTabs->setTabText(0, tr("Alts"));
|
||||
sectionTabs->setTabText(1, tr("Sessions"));
|
||||
sectionTabs->setTabText(2, tr("Staff Last Logins"));
|
||||
|
||||
if (cachedUserInfoValid) {
|
||||
applyUserInfo();
|
||||
}
|
||||
if (cachedAltsCount >= 0) {
|
||||
applyAltsCount();
|
||||
}
|
||||
|
||||
altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"),
|
||||
tr("Warns"), tr("Bans"), tr("Active")});
|
||||
altsTable->setToolTip(tr("Double-click or press Enter on a row to open it in the User Alts tab"));
|
||||
sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")});
|
||||
staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")});
|
||||
}
|
||||
|
|
@ -237,11 +273,15 @@ QString TabModeration::formatEpoch(quint64 ts) const
|
|||
void TabModeration::clearUserData()
|
||||
{
|
||||
currentUser.clear();
|
||||
cachedUserInfoValid = false;
|
||||
cachedAltsCount = -1;
|
||||
userInfoNameValue->clear();
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
sublineLabel->clear();
|
||||
cardReportsValue->setText(QStringLiteral("–"));
|
||||
cardWarnsValue->setText(QStringLiteral("–"));
|
||||
cardBansValue->setText(QStringLiteral("–"));
|
||||
altsCountValue->setText(QStringLiteral("–"));
|
||||
userInfoNotesEdit->clear();
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
|
|
@ -267,10 +307,13 @@ void TabModeration::investigateUser()
|
|||
return;
|
||||
}
|
||||
currentUser = userName;
|
||||
cachedUserInfoValid = false;
|
||||
cachedAltsCount = -1;
|
||||
resetPasswordButton->setEnabled(true);
|
||||
removeAvatarButton->setEnabled(true);
|
||||
removeBannerButton->setEnabled(true);
|
||||
openAltsButton->setEnabled(true);
|
||||
altsCountValue->setText(QStringLiteral("…"));
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
requestUserInfo(userName);
|
||||
|
|
@ -289,10 +332,11 @@ void TabModeration::openAltsGraph()
|
|||
void TabModeration::requestUserInfo(const QString &userName)
|
||||
{
|
||||
userInfoNameValue->setText(userName);
|
||||
userInfoRegisteredValue->setText(tr("Loading..."));
|
||||
userInfoLastLoginValue->setText(tr("Loading..."));
|
||||
sublineLabel->setText(tr("Loading..."));
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
cardReportsValue->setText(QStringLiteral("…"));
|
||||
cardWarnsValue->setText(QStringLiteral("…"));
|
||||
cardBansValue->setText(QStringLiteral("…"));
|
||||
userInfoNotesEdit->clear();
|
||||
|
||||
Command_ReportUserInfo cmd;
|
||||
|
|
@ -305,9 +349,12 @@ void TabModeration::requestUserInfo(const QString &userName)
|
|||
void TabModeration::userInfoResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
cachedUserInfoValid = false;
|
||||
sublineLabel->clear();
|
||||
userInfoStatusValue->setText(tr("Error loading user info."));
|
||||
cardReportsValue->setText(QStringLiteral("–"));
|
||||
cardWarnsValue->setText(QStringLiteral("–"));
|
||||
cardBansValue->setText(QStringLiteral("–"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -316,20 +363,28 @@ void TabModeration::userInfoResponse(const Response &response)
|
|||
return;
|
||||
}
|
||||
|
||||
userInfoRegisteredValue->setText(formatEpoch(resp.registration_time()));
|
||||
userInfoLastLoginValue->setText(formatEpoch(resp.last_login()));
|
||||
cachedUserInfo.CopyFrom(resp);
|
||||
cachedUserInfoValid = true;
|
||||
applyUserInfo();
|
||||
}
|
||||
|
||||
void TabModeration::applyUserInfo()
|
||||
{
|
||||
const Response_ReportUserInfo &resp = cachedUserInfo;
|
||||
sublineLabel->setText(tr("Registered %1 · Last login %2")
|
||||
.arg(formatEpoch(resp.registration_time()))
|
||||
.arg(formatEpoch(resp.last_login())));
|
||||
|
||||
QStringList statusParts;
|
||||
statusParts << (resp.is_active() ? tr("active") : tr("inactive"));
|
||||
if (resp.has_is_admin() && resp.is_admin()) {
|
||||
statusParts << tr("admin");
|
||||
}
|
||||
userInfoStatusValue->setText(statusParts.join(", "));
|
||||
userInfoStatusValue->setText(statusParts.join(QStringLiteral(", ")));
|
||||
|
||||
userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_bans())
|
||||
.arg(resp.total_warns()));
|
||||
cardReportsValue->setText(QString::number(resp.total_reports()));
|
||||
cardWarnsValue->setText(QString::number(resp.total_warns()));
|
||||
cardBansValue->setText(QString::number(resp.total_bans()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
|
|
@ -338,12 +393,21 @@ void TabModeration::userInfoResponse(const Response &response)
|
|||
}
|
||||
}
|
||||
|
||||
void TabModeration::applyAltsCount()
|
||||
{
|
||||
altsCountValue->setText(tr("%1 related").arg(cachedAltsCount));
|
||||
}
|
||||
|
||||
void TabModeration::requestSessions(const QString &userName)
|
||||
{
|
||||
Command_GetUserSessions cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse);
|
||||
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) {
|
||||
if (userName == currentUser) {
|
||||
sessionsResponse(response);
|
||||
}
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +439,11 @@ void TabModeration::requestAlts(const QString &userName)
|
|||
Command_GetUserAlts cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse);
|
||||
connect(pend, &PendingCommand::finished, this, [this, userName](const Response &response) {
|
||||
if (userName == currentUser) {
|
||||
altsResponse(response);
|
||||
}
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
|
|
@ -383,10 +451,21 @@ void TabModeration::altsResponse(const Response &response)
|
|||
{
|
||||
altsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
cachedAltsCount = -1;
|
||||
altsCountValue->setText(QStringLiteral("–"));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
|
||||
// The server always includes the frontier user as a depth-0 seed row.
|
||||
int relatedCount = 0;
|
||||
for (int i = 0; i < resp.alts_size(); ++i) {
|
||||
if (!resp.alts(i).has_depth() || resp.alts(i).depth() > 0) {
|
||||
++relatedCount;
|
||||
}
|
||||
}
|
||||
cachedAltsCount = relatedCount;
|
||||
applyAltsCount();
|
||||
altsTable->setRowCount(resp.alts_size());
|
||||
for (int i = 0; i < resp.alts_size(); ++i) {
|
||||
const ServerInfo_UserAlt &alt = resp.alts(i);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class AbstractClient;
|
||||
|
|
@ -13,6 +14,7 @@ class QLabel;
|
|||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QTableWidget;
|
||||
class QTabWidget;
|
||||
class QTextEdit;
|
||||
class Response;
|
||||
class TabSupervisor;
|
||||
|
|
@ -54,10 +56,15 @@ private:
|
|||
void requestAlts(const QString &userName);
|
||||
void requestModeratorLogins();
|
||||
void clearUserData();
|
||||
void applyUserInfo();
|
||||
void applyAltsCount();
|
||||
[[nodiscard]] QString formatEpoch(quint64 ts) const;
|
||||
|
||||
AbstractClient *client;
|
||||
QString currentUser;
|
||||
Response_ReportUserInfo cachedUserInfo;
|
||||
bool cachedUserInfoValid = false;
|
||||
int cachedAltsCount = -1;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QPushButton *investigateButton;
|
||||
|
|
@ -66,26 +73,25 @@ private:
|
|||
QPushButton *removeBannerButton;
|
||||
|
||||
QGroupBox *userInfoGroup;
|
||||
QLabel *userInfoNameLabel;
|
||||
QLabel *userInfoNameValue;
|
||||
QLabel *userInfoRegisteredLabel;
|
||||
QLabel *userInfoRegisteredValue;
|
||||
QLabel *userInfoLastLoginLabel;
|
||||
QLabel *userInfoLastLoginValue;
|
||||
QLabel *userInfoStatusLabel;
|
||||
QLabel *userInfoStatusValue;
|
||||
QLabel *userInfoCountsLabel;
|
||||
QLabel *userInfoCountsValue;
|
||||
QLabel *sublineLabel;
|
||||
QGroupBox *cardReports;
|
||||
QGroupBox *cardWarns;
|
||||
QGroupBox *cardBans;
|
||||
QGroupBox *altsCard;
|
||||
QLabel *cardReportsValue;
|
||||
QLabel *cardWarnsValue;
|
||||
QLabel *cardBansValue;
|
||||
QLabel *altsCountValue;
|
||||
QLabel *userInfoNotesLabel;
|
||||
QTextEdit *userInfoNotesEdit;
|
||||
|
||||
QGroupBox *altsGroup;
|
||||
QTabWidget *sectionTabs;
|
||||
QTableWidget *altsTable;
|
||||
QPushButton *openAltsButton;
|
||||
QGroupBox *sessionsGroup;
|
||||
QTableWidget *sessionsTable;
|
||||
QGroupBox *staffGroup;
|
||||
QTableWidget *staffTable;
|
||||
QPushButton *openAltsButton;
|
||||
QPushButton *refreshStaffButton;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,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 <QAbstractItemView>
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
#include <QPushButton>
|
||||
#include <QSharedPointer>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QTextCursor>
|
||||
|
|
@ -126,26 +127,43 @@ 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);
|
||||
applyChatLogFont();
|
||||
connect(themeManager, &ThemeManager::fontsChanged, this, &TabReport::applyChatLogFont);
|
||||
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;
|
||||
|
|
@ -156,12 +174,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;
|
||||
|
|
@ -180,7 +198,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);
|
||||
|
|
@ -196,7 +214,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);
|
||||
|
|
@ -223,13 +240,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;
|
||||
|
|
@ -261,12 +276,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();
|
||||
|
|
@ -293,18 +310,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:"));
|
||||
|
|
@ -312,10 +329,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"));
|
||||
|
||||
|
|
@ -408,7 +433,8 @@ void TabReport::reportListResponse(const Response &response)
|
|||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
clearReportHeader();
|
||||
clearUserContext();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,11 +471,14 @@ void TabReport::applyFilters()
|
|||
table->setSortingEnabled(false);
|
||||
table->setRowCount(filteredReports.size());
|
||||
|
||||
QFont reportedFont = table->font();
|
||||
reportedFont.setBold(true);
|
||||
|
||||
for (int row = 0; row < filteredReports.size(); ++row) {
|
||||
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())));
|
||||
|
||||
|
|
@ -458,12 +487,30 @@ void TabReport::applyFilters()
|
|||
|
||||
table->setItem(row, COL_ROOM,
|
||||
new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : ""));
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
|
|
@ -490,6 +537,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();
|
||||
|
|
@ -500,7 +572,8 @@ void TabReport::onSelectionChanged()
|
|||
commentInput->clear();
|
||||
commentButton->setEnabled(false);
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
clearReportHeader();
|
||||
clearUserContext();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -509,6 +582,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);
|
||||
|
|
@ -546,12 +631,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());
|
||||
|
|
@ -871,12 +961,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();
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ class QGroupBox;
|
|||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSplitter;
|
||||
class QTableWidget;
|
||||
class QTabWidget;
|
||||
class QTextEdit;
|
||||
class QTimer;
|
||||
class GameReplay;
|
||||
|
|
@ -72,6 +72,9 @@ private:
|
|||
void applyFilters();
|
||||
void updateStats();
|
||||
void loadReportDetails(int reportId);
|
||||
void updateReportHeader(const ServerInfo_Report &report);
|
||||
void clearReportHeader();
|
||||
void clearUserContext();
|
||||
|
||||
AbstractClient *client;
|
||||
|
||||
|
|
@ -81,10 +84,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;
|
||||
|
|
@ -100,7 +103,6 @@ private:
|
|||
QTimer *refreshTimer;
|
||||
UserContextMenu *userContextMenu;
|
||||
|
||||
QGroupBox *userContextGroup;
|
||||
QLabel *userContextName;
|
||||
QLabel *userContextAccountAge;
|
||||
QLabel *userContextReports;
|
||||
|
|
@ -125,6 +127,7 @@ private:
|
|||
|
||||
QList<ServerInfo_Report> allReports;
|
||||
QList<ServerInfo_Report> filteredReports;
|
||||
int headerReportId = -1;
|
||||
int detailsRequestedReportId = -1;
|
||||
int detailsRequestSeq = 0;
|
||||
int selectedReportIdBeforeRefresh = -1;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,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,
|
||||
|
|
@ -89,7 +106,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());
|
||||
|
|
@ -102,8 +120,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())));
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ QFont reportLogFont();
|
|||
|
||||
QString formatReportCategory(const QString &category);
|
||||
QString formatReportTime(qint64 secondsSinceEpoch);
|
||||
QString formatReportStatus(const QString &status);
|
||||
|
||||
void fillReportTableRow(QTableWidget *table,
|
||||
int row,
|
||||
|
|
@ -30,7 +31,8 @@ void fillReportTableRow(QTableWidget *table,
|
|||
int categoryColumn,
|
||||
int gameIdColumn,
|
||||
int statusColumn,
|
||||
int assignedColumn);
|
||||
int assignedColumn,
|
||||
bool colorizeStatus = true);
|
||||
|
||||
void renderReportDetails(QTextEdit *chatLogEdit,
|
||||
QTextEdit *commentsEdit,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
#include "table_activation.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
#include <QShortcut>
|
||||
#include <QTableWidget>
|
||||
|
||||
namespace
|
||||
{
|
||||
void addActivationShortcut(QTableWidget *table,
|
||||
QObject *context,
|
||||
int key,
|
||||
const std::function<void(const QModelIndex &)> &handler)
|
||||
{
|
||||
auto *shortcut = new QShortcut(QKeySequence(key), table);
|
||||
// The table's viewport takes focus, so match children as well.
|
||||
shortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
||||
QObject::connect(shortcut, &QShortcut::activated, context, [table, handler] { handler(table->currentIndex()); });
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void connectTableActivation(QTableWidget *table,
|
||||
QObject *context,
|
||||
const std::function<void(const QModelIndex &)> &handler)
|
||||
{
|
||||
QObject::connect(table, &QTableWidget::doubleClicked, context, handler);
|
||||
addActivationShortcut(table, context, Qt::Key_Return, handler);
|
||||
addActivationShortcut(table, context, Qt::Key_Enter, handler);
|
||||
}
|
||||
32
cockatrice/src/interface/widgets/utility/table_activation.h
Normal file
32
cockatrice/src/interface/widgets/utility/table_activation.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @file table_activation.h
|
||||
* @brief Row activation for tables that must not navigate on a single click.
|
||||
*/
|
||||
|
||||
#ifndef TABLE_ACTIVATION_H
|
||||
#define TABLE_ACTIVATION_H
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QModelIndex;
|
||||
class QObject;
|
||||
class QTableWidget;
|
||||
|
||||
/**
|
||||
* @brief Call @p handler on double-click or Enter, never on a single click.
|
||||
*
|
||||
* QAbstractItemView::activated fires on a single click on platforms where
|
||||
* SH_ItemView_ActivateItemOnSingleClick is set, so navigation actions must not
|
||||
* use it. This connects doubleClicked and installs Return/Enter shortcuts that
|
||||
* forward the current index.
|
||||
*
|
||||
* @param table The table whose rows activate.
|
||||
* @param context Object that owns the connections; the handler is disconnected
|
||||
* with it.
|
||||
* @param handler Receives the activated row's index, which may be invalid.
|
||||
*/
|
||||
void connectTableActivation(QTableWidget *table,
|
||||
QObject *context,
|
||||
const std::function<void(const QModelIndex &)> &handler);
|
||||
|
||||
#endif // TABLE_ACTIVATION_H
|
||||
Loading…
Add table
Add a link
Reference in a new issue