diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 66cbe240a..80c5a09dc 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -303,6 +303,8 @@ set(cockatrice_SOURCES src/interface/widgets/settings_page/user_interface_settings_page.cpp src/interface/widgets/utility/custom_line_edit.cpp 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 diff --git a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp index aa9ce907c..3cc412a7e 100644 --- a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp +++ b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.cpp @@ -1,5 +1,7 @@ #include "alt_table_delegate.h" +#include "../utility/pill_painter.h" + #include #include #include @@ -9,9 +11,6 @@ namespace { -constexpr qreal PILL_ALPHA = 0.30; -constexpr int PILL_PAD_X = 10; -constexpr int PILL_PAD_Y = 4; constexpr int ICON_TEXT_GAP = 4; } // namespace @@ -21,11 +20,6 @@ AltTableDelegate::AltTableDelegate(int viaColumn, int warnsColumn, int bansColum { } -void AltTableDelegate::setVpnThreshold(int threshold) -{ - vpnThreshold = threshold; -} - void AltTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const bool selected = (option.state & QStyle::State_Selected) != 0; @@ -37,58 +31,27 @@ void AltTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opti const QString text = index.data(Qt::DisplayRole).toString(); const QPalette::ColorRole emphasisRole = selected ? QPalette::HighlightedText : QPalette::WindowText; - auto paintPill = [&](QPalette::ColorRole tintRole, QPalette::ColorRole textRole, const QIcon &icon, - const QString &pillText) { - 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(); - } - - QRect textRect = pillRect; - if (!icon.isNull()) { - const int iconSize = qMin(pillRect.height(), pillRect.width()); - if (iconSize > 0) { - const QRect iconRect(pillRect.left() + ICON_TEXT_GAP, pillRect.top(), iconSize, iconSize); - icon.paint(painter, iconRect); - textRect.setLeft(iconRect.right() + ICON_TEXT_GAP); - } - } - const QString elided = opt.fontMetrics.elidedText(pillText, opt.textElideMode, textRect.width()); - style->drawItemText(painter, textRect, Qt::AlignCenter, opt.palette, true, elided, textRole); - }; - if (index.column() == viaColumn) { bool sharedOk = false; const unsigned shared = index.sibling(index.row(), sharedColumn).data(Qt::DisplayRole).toUInt(&sharedOk); - const bool dimmed = sharedOk && text.startsWith(QStringLiteral("ip:")) && shared > 1; - const bool vpn = - sharedOk && text.startsWith(QStringLiteral("ip:")) && shared > static_cast(vpnThreshold); + 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")) { - paintPill(QPalette::Link, emphasisRole, QIcon(), text); - return; + tintRole = QPalette::Link; + } else if (text.startsWith(QStringLiteral("clientid:"))) { + tintRole = QPalette::Highlight; + } else if (ipMatch) { + tintRole = QPalette::PlaceholderText; } - if (text.startsWith(QStringLiteral("clientid:"))) { - paintPill(QPalette::Highlight, emphasisRole, QIcon(), text); - return; - } - if (text.startsWith(QStringLiteral("ip:"))) { - if (vpn) { - paintPill(QPalette::Highlight, textRole, style->standardIcon(QStyle::SP_MessageBoxWarning), text); - return; + if (tintRole != QPalette::NoRole) { + if (!pill_painter::paint(painter, opt, tintRole, textRole, text)) { + QStyledItemDelegate::paint(painter, opt, index); } - paintPill(QPalette::PlaceholderText, textRole, QIcon(), text); return; } QStyledItemDelegate::paint(painter, opt, index); @@ -123,7 +86,7 @@ QSize AltTableDelegate::sizeHint(const QStyleOptionViewItem &option, const QMode { const QSize hint = QStyledItemDelegate::sizeHint(option, index); if (index.column() == viaColumn) { - return hint + QSize(PILL_PAD_X + 2 * ICON_TEXT_GAP, PILL_PAD_Y); + 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); diff --git a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h index 7d7af2928..73386b00d 100644 --- a/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h +++ b/cockatrice/src/interface/widgets/tabs/alt_table_delegate.h @@ -6,10 +6,10 @@ /** * 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), - * with the style's warning icon once an IP is shared by more than the VPN - * threshold. Warn/ban counts gain the style's warning/critical icons when - * nonzero. Selected rows and unknown content fall back to default painting. + * 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 { @@ -20,7 +20,6 @@ public: int bansColumn, int sharedColumn, QObject *parent = nullptr); - void setVpnThreshold(int threshold); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; @@ -29,7 +28,6 @@ private: int warnsColumn; int bansColumn; int sharedColumn; - int vpnThreshold = 10; }; #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 a7ee12c2c..0630cd5a8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.cpp @@ -106,9 +106,12 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q 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) { - investigate(userName); + goToTrailEntry(userName); } }); @@ -146,8 +149,6 @@ TabAlts::TabAlts(TabSupervisor *_tabSupervisor, AbstractClient *_client, const Q altsTable = new QTableWidget(0, COL_ALTS_COUNT); configureAltTable(altsTable); altsDelegate = new AltTableDelegate(COL_ALTS_VIA, COL_ALTS_WARNS, COL_ALTS_BANS, COL_ALTS_SHARED, altsTable); - altsDelegate->setVpnThreshold(vpnSpin->value()); - connect(vpnSpin, &QSpinBox::valueChanged, altsDelegate, &AltTableDelegate::setVpnThreshold); altsTable->setItemDelegateForColumn(COL_ALTS_VIA, altsDelegate); altsTable->setItemDelegateForColumn(COL_ALTS_WARNS, altsDelegate); altsTable->setItemDelegateForColumn(COL_ALTS_BANS, altsDelegate); @@ -273,6 +274,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()) { @@ -438,6 +453,8 @@ 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() > 2) { for (int col = 0; col < COL_ALTS_COUNT; ++col) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_alts.h b/cockatrice/src/interface/widgets/tabs/tab_alts.h index 4ba355447..50aabf8f5 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_alts.h +++ b/cockatrice/src/interface/widgets/tabs/tab_alts.h @@ -52,6 +52,7 @@ 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); 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