diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 4707c3f32..fc593058a 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -287,8 +287,12 @@ set(cockatrice_SOURCES src/interface/widgets/utility/custom_line_edit.cpp src/interface/widgets/utility/epoch_format.h src/interface/widgets/utility/get_text_with_max.cpp + src/interface/widgets/utility/pill_painter.cpp + src/interface/widgets/utility/pill_painter.h 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 @@ -376,6 +380,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/alt_table_delegate.cpp src/interface/widgets/tabs/report_status_delegate.cpp src/interface/widgets/tabs/tab.cpp src/interface/widgets/tabs/tab_account.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp index 2a2758ba5..d5c4b62be 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp @@ -223,7 +223,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); diff --git a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp new file mode 100644 index 000000000..3cc412a7e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp @@ -0,0 +1,95 @@ +#include "alt_table_delegate.h" + +#include "../utility/pill_painter.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr int ICON_TEXT_GAP = 4; +} // namespace + +AltTableDelegate::AltTableDelegate(int viaColumn, int warnsColumn, int bansColumn, int sharedColumn, QObject *parent) + : QStyledItemDelegate(parent), viaColumn(viaColumn), warnsColumn(warnsColumn), bansColumn(bansColumn), + sharedColumn(sharedColumn) +{ +} + +void AltTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const bool selected = (option.state & QStyle::State_Selected) != 0; + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + painter->setFont(opt.font); + QStyle *style = opt.widget != nullptr ? opt.widget->style() : QApplication::style(); + const QString text = index.data(Qt::DisplayRole).toString(); + const QPalette::ColorRole emphasisRole = selected ? QPalette::HighlightedText : QPalette::WindowText; + + if (index.column() == viaColumn) { + bool sharedOk = false; + const unsigned shared = index.sibling(index.row(), sharedColumn).data(Qt::DisplayRole).toUInt(&sharedOk); + const bool ipMatch = text.startsWith(QStringLiteral("ip:")); + // shared_count for an IP match already includes the investigated account, + // so anything above 2 means the address is shared beyond that pair. + const bool dimmed = sharedOk && ipMatch && shared > 2; + const QPalette::ColorRole textRole = (dimmed && !selected) ? QPalette::PlaceholderText : emphasisRole; + + QPalette::ColorRole tintRole = QPalette::NoRole; + if (text == QStringLiteral("email")) { + tintRole = QPalette::Link; + } else if (text.startsWith(QStringLiteral("clientid:"))) { + tintRole = QPalette::Highlight; + } else if (ipMatch) { + tintRole = QPalette::PlaceholderText; + } + if (tintRole != QPalette::NoRole) { + if (!pill_painter::paint(painter, opt, tintRole, textRole, text)) { + QStyledItemDelegate::paint(painter, opt, index); + } + return; + } + QStyledItemDelegate::paint(painter, opt, index); + return; + } + + if (index.column() == warnsColumn || index.column() == bansColumn) { + bool countOk = false; + const int count = text.toInt(&countOk); + if (countOk && count > 0 && !selected) { + const QIcon icon = style->standardIcon(index.column() == bansColumn ? QStyle::SP_MessageBoxCritical + : QStyle::SP_MessageBoxWarning); + const int iconSize = qMax(0, opt.rect.height() - 2 * ICON_TEXT_GAP); + QRect textRect = opt.rect; + if (!icon.isNull() && iconSize > 0) { + const QRect iconRect(opt.rect.left() + ICON_TEXT_GAP, opt.rect.top() + ICON_TEXT_GAP, iconSize, + iconSize); + icon.paint(painter, iconRect); + textRect.setLeft(iconRect.right() + ICON_TEXT_GAP); + } + const QString elided = opt.fontMetrics.elidedText(text, opt.textElideMode, textRect.width()); + style->drawItemText(painter, textRect, Qt::AlignLeft | Qt::AlignVCenter, opt.palette, true, elided, + emphasisRole); + return; + } + } + + QStyledItemDelegate::paint(painter, opt, index); +} + +QSize AltTableDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const QSize hint = QStyledItemDelegate::sizeHint(option, index); + if (index.column() == viaColumn) { + return hint + QSize(pill_painter::PAD_X + 2 * ICON_TEXT_GAP, pill_painter::PAD_Y); + } + if (index.column() == warnsColumn || index.column() == bansColumn) { + return hint + QSize(2 * ICON_TEXT_GAP, 0); + } + return hint; +} diff --git a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h new file mode 100644 index 000000000..73386b00d --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h @@ -0,0 +1,33 @@ +#ifndef ALT_TABLE_DELEGATE_H +#define ALT_TABLE_DELEGATE_H + +#include +#include + +/** + * Badge + icon painting for the User Alts table. The match-evidence column + * renders as a palette-tinted pill per match type (email / client id / IP), and + * an IP shared by more than one other account is dimmed. Warn/ban counts gain + * the style's warning/critical icons when nonzero. Selected rows and unknown + * content fall back to default painting. + */ +class AltTableDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit AltTableDelegate(int viaColumn, + int warnsColumn, + int bansColumn, + int sharedColumn, + 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; + +private: + int viaColumn; + int warnsColumn; + int bansColumn; + int sharedColumn; +}; + +#endif // ALT_TABLE_DELEGATE_H diff --git a/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp b/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp index fe59702c4..62e427e0e 100644 --- a/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp +++ b/cockatrice/src/interface/widgets/tabs/report_status_delegate.cpp @@ -1,18 +1,13 @@ #include "report_status_delegate.h" +#include "../utility/pill_painter.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) { } @@ -46,32 +41,14 @@ void ReportStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & 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); + if (!pill_painter::paint(painter, opt, tintRole, textRole, opt.text.isEmpty() ? status : opt.text)) { + QStyledItemDelegate::paint(painter, opt, index); + } } 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); + return hint + QSize(pill_painter::PAD_X, pill_painter::PAD_Y); } diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp index ec32bf60a..833584a39 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp @@ -1,7 +1,9 @@ #include "tab_alts.h" #include "../utility/epoch_format.h" +#include "../utility/table_activation.h" #include "abstract_client.h" +#include "alt_table_delegate.h" #include "libcockatrice/protocol/pb/response.pb.h" #include "libcockatrice/protocol/pb/response_user_alts.pb.h" @@ -101,6 +103,18 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q connect(backButton, &QPushButton::clicked, this, &TabAlts::goBack); statusLabel = new QLabel; + trailLabel = new QLabel; + trailLabel->setTextFormat(Qt::RichText); + trailLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + trailLabel->setOpenExternalLinks(false); + // The trail grows with every hop, so let it wrap instead of forcing the tab + // wider than the window. + trailLabel->setWordWrap(true); + connect(trailLabel, &QLabel::linkActivated, this, [this](const QString &userName) { + if (!userName.isEmpty() && userName != currentUser) { + goToTrailEntry(userName); + } + }); auto *topBar = new QHBoxLayout; topBar->addWidget(backButton); @@ -135,7 +149,11 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureAltTable(altsTable); - connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) { + altsDelegate = new AltTableDelegate(COL_ALTS_VIA, COL_ALTS_WARNS, COL_ALTS_BANS, COL_ALTS_SHARED, altsTable); + altsTable->setItemDelegateForColumn(COL_ALTS_VIA, altsDelegate); + altsTable->setItemDelegateForColumn(COL_ALTS_WARNS, altsDelegate); + altsTable->setItemDelegateForColumn(COL_ALTS_BANS, altsDelegate); + connectTableActivation(altsTable, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } @@ -173,6 +191,10 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q detailsLayout->addLayout(detailGrid); detailsLayout->addWidget(warnsTable, 1); detailsLayout->addWidget(bansTable, 1); + investigateSelectedButton = new QPushButton; + investigateSelectedButton->setEnabled(false); + connect(investigateSelectedButton, &QPushButton::clicked, this, &TabAlts::investigateSelected); + detailsLayout->addWidget(investigateSelectedButton); auto *contentSplitter = new QSplitter(Qt::Horizontal); contentSplitter->addWidget(altsTable); @@ -185,6 +207,7 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q mainLayout->addLayout(topBar); mainLayout->addLayout(filterBar); mainLayout->addWidget(statusLabel); + mainLayout->addWidget(trailLabel); mainLayout->addWidget(contentSplitter, 1); retranslateUi(); @@ -224,6 +247,8 @@ void TabAlts::retranslateUi() detailNameLabel->setText(tr("Name:")); detailCountsLabel->setText(tr("Counts:")); detailNotesLabel->setText(tr("Admin notes:")); + investigateSelectedButton->setText(tr("Investigate this user")); + investigateSelectedButton->setToolTip(tr("Open the selected account as a new investigation")); if (!lastAlts.isEmpty()) { renderTable(); @@ -240,6 +265,13 @@ void TabAlts::investigate(const QString &userName) if (userName.isEmpty() || userName == currentUser) { return; } + // Alt matches are symmetric, so revisiting an earlier account is a normal + // double-click. Truncate there instead of pushing it again, so the trail + // stays a single path. + if (backStack.contains(userName)) { + goToTrailEntry(userName); + return; + } if (!currentUser.isEmpty()) { backStack << currentUser; backButton->setEnabled(true); @@ -247,6 +279,20 @@ void TabAlts::investigate(const QString &userName) loadUser(userName); } +void TabAlts::goToTrailEntry(const QString &userName) +{ + const int index = backStack.indexOf(userName); + if (index < 0) { + investigate(userName); + return; + } + // Clicking an earlier crumb truncates the history there instead of pushing + // another entry, so the trail keeps a single path and Back still works. + backStack.erase(backStack.begin() + index, backStack.end()); + backButton->setEnabled(!backStack.isEmpty()); + loadUser(userName); +} + void TabAlts::goBack() { if (backStack.isEmpty()) { @@ -261,16 +307,41 @@ void TabAlts::loadUser(const QString &userName) { currentUser = userName; searchEdit->setText(userName); + updateTrail(); investigateUser(); } +void TabAlts::updateTrail() +{ + QStringList trail = backStack; + trail << currentUser; + trail.removeAll(QString()); + QStringList links; + for (const QString &name : trail) { + const QString safeName = name.toHtmlEscaped(); + if (name == currentUser) { + links << QStringLiteral("") + safeName + QStringLiteral(""); + } else { + links << QStringLiteral("") + safeName + + QStringLiteral(""); + } + } + trailLabel->setText(links.join(QStringLiteral(" › "))); + trailLabel->setVisible(!links.isEmpty()); +} + void TabAlts::investigateUser() { const QString userName = searchEdit->text().simplified(); if (userName.isEmpty()) { return; } + if (userName != currentUser && !currentUser.isEmpty()) { + backStack << currentUser; + backButton->setEnabled(true); + } currentUser = userName; + updateTrail(); lastAlts.clear(); selectedUser.clear(); clearDetails(); @@ -392,9 +463,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"))); + // shared_count already includes the investigated account, so anything + // above 2 means the address is shared beyond that pair. if (QString::fromStdString(alt.match_reason()).startsWith(QStringLiteral("ip:")) && alt.has_shared_count() && - alt.shared_count() > 1) { + alt.shared_count() > 2) { for (int col = 0; col < COL_ALTS_COUNT; ++col) { + if (col == COL_ALTS_VIA) { + continue; + } if (auto *item = altsTable->item(row, col)) { item->setForeground(dimColor); } @@ -445,6 +521,13 @@ void TabAlts::onFilterChanged() renderTable(); } +void TabAlts::investigateSelected() +{ + if (!selectedUser.isEmpty() && selectedUser != currentUser) { + investigate(selectedUser); + } +} + void TabAlts::onAltSelected() { const auto selected = altsTable->selectedItems(); @@ -468,6 +551,7 @@ void TabAlts::requestDetails(const QString &userName) detailNotesEdit->clear(); warnsTable->setRowCount(0); bansTable->setRowCount(0); + investigateSelectedButton->setEnabled(userName != currentUser); Command_ReportUserInfo infoCmd; infoCmd.set_user_name(userName.toStdString()); @@ -507,6 +591,7 @@ void TabAlts::clearDetails() detailNotesEdit->clear(); warnsTable->setRowCount(0); bansTable->setRowCount(0); + investigateSelectedButton->setEnabled(false); } void TabAlts::userDetailsResponse(const Response &response) diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.h b/cockatrice/src/interface/widgets/tabs/tab_alts.h index 8c0086f25..50aabf8f5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.h +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.h @@ -10,6 +10,7 @@ #include class AbstractClient; +class AltTableDelegate; class QGroupBox; class QLabel; class QLineEdit; @@ -41,6 +42,7 @@ public: private slots: void investigateUser(); void goBack(); + void investigateSelected(); void altsResponse(const Response &response); void onFilterChanged(); void onAltSelected(); @@ -50,6 +52,8 @@ private slots: private: void loadUser(const QString &userName); + void goToTrailEntry(const QString &userName); + void updateTrail(); void requestAlts(const QString &userName, int depth); void requestDetails(const QString &userName); void clearDetails(); @@ -67,8 +71,11 @@ private: QSpinBox *depthSpin; QPushButton *investigateButton; QPushButton *backButton; + QPushButton *investigateSelectedButton; + QLabel *trailLabel; QLabel *statusLabel; QTableWidget *altsTable; + AltTableDelegate *altsDelegate; QPushButton *emailChip; QPushButton *clientIdChip; diff --git a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp index 747eb7600..8568b0d2e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_moderation.cpp @@ -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" @@ -176,7 +177,7 @@ TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_cli altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureTable(altsTable); - connect(altsTable, &QTableWidget::activated, this, [this](const QModelIndex &index) { + connectTableActivation(altsTable, this, [this](const QModelIndex &index) { if (!index.isValid()) { return; } @@ -456,7 +457,14 @@ void TabModeration::altsResponse(const Response &response) } const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext); - cachedAltsCount = resp.alts_size(); + // 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) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_report.cpp b/cockatrice/src/interface/widgets/tabs/tab_report.cpp index ea2f47902..0dda1ed66 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_report.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_report.cpp @@ -463,6 +463,9 @@ 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]; @@ -476,11 +479,7 @@ void TabReport::applyFilters() table->setItem(row, COL_ROOM, 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); } diff --git a/cockatrice/src/interface/widgets/utility/pill_painter.cpp b/cockatrice/src/interface/widgets/utility/pill_painter.cpp new file mode 100644 index 000000000..6f317b951 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/pill_painter.cpp @@ -0,0 +1,41 @@ +#include "pill_painter.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr qreal PILL_ALPHA = 0.30; +} // namespace + +bool pill_painter::paint(QPainter *painter, + const QStyleOptionViewItem &option, + QPalette::ColorRole tintRole, + QPalette::ColorRole textRole, + const QString &text) +{ + const QRect pillRect = option.rect.adjusted(PAD_X / 2, PAD_Y / 2, -PAD_X / 2, -PAD_Y / 2); + if (pillRect.width() <= 0 || pillRect.height() <= 0) { + return false; + } + + if ((option.state & QStyle::State_Selected) == 0) { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + QColor pill = option.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(); + } + + QStyle *style = option.widget != nullptr ? option.widget->style() : QApplication::style(); + const QString elided = option.fontMetrics.elidedText(text, option.textElideMode, pillRect.width()); + style->drawItemText(painter, pillRect, Qt::AlignCenter, option.palette, true, elided, textRole); + return true; +} diff --git a/cockatrice/src/interface/widgets/utility/pill_painter.h b/cockatrice/src/interface/widgets/utility/pill_painter.h new file mode 100644 index 000000000..72e3fe0b4 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/pill_painter.h @@ -0,0 +1,29 @@ +#ifndef PILL_PAINTER_H +#define PILL_PAINTER_H + +#include +#include + +class QPainter; +class QStyleOptionViewItem; + +namespace pill_painter +{ +constexpr int PAD_X = 10; +constexpr int PAD_Y = 4; + +/** + * Draws a translucent, palette-tinted rounded pill inside the option's rect and + * paints the text elided and centred on it, so badges follow the active theme. + * Returns false when the cell is too small to hold a pill, leaving the caller to + * fall back to default painting. + */ +bool paint(QPainter *painter, + const QStyleOptionViewItem &option, + QPalette::ColorRole tintRole, + QPalette::ColorRole textRole, + const QString &text); + +} // namespace pill_painter + +#endif // PILL_PAINTER_H diff --git a/cockatrice/src/interface/widgets/utility/table_activation.cpp b/cockatrice/src/interface/widgets/utility/table_activation.cpp new file mode 100644 index 000000000..5633b9f58 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/table_activation.cpp @@ -0,0 +1,28 @@ +#include "table_activation.h" + +#include +#include +#include + +namespace +{ +void addActivationShortcut(QTableWidget *table, + QObject *context, + int key, + const std::function &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 &handler) +{ + QObject::connect(table, &QTableWidget::doubleClicked, context, handler); + addActivationShortcut(table, context, Qt::Key_Return, handler); + addActivationShortcut(table, context, Qt::Key_Enter, handler); +} diff --git a/cockatrice/src/interface/widgets/utility/table_activation.h b/cockatrice/src/interface/widgets/utility/table_activation.h new file mode 100644 index 000000000..f5038bdb0 --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/table_activation.h @@ -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 + +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 &handler); + +#endif // TABLE_ACTIVATION_H