From fc453c68a70d374632977882f35d188775f64359 Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Mon, 16 Mar 2026 18:44:29 -0400 Subject: [PATCH] Add card selection counter (#6685) * feat(game): add drag selection counter overlay Display count of selected cards inside the lasso during drag selection. Count appears near cursor, repositioning to stay within selection bounds. Includes SelectionRubberBand subclass to allow label to appear above band. QRubberBand calls raise() in showEvent/changeEvent to stay on top - this subclass suppresses that behavior so dragCountLabel can be visible. Adds user setting to enable/disable the drag count overlay. * feat(game): add persistent selection counter overlay. Display total count of selected cards in bottom-right corner when multiple cards are selected. Updates on selection changes and window resize. The counter connects to QGraphicsScene::selectionChanged to stay up-to-date without requiring manual refresh. Adds user setting to enable/disable the total count overlay. --------- Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com> --- .../src/client/settings/cache_settings.cpp | 15 +++ .../src/client/settings/cache_settings.h | 12 ++ cockatrice/src/game/game_scene.cpp | 4 +- cockatrice/src/game/game_scene.h | 4 +- cockatrice/src/game/game_view.cpp | 121 +++++++++++++++++- cockatrice/src/game/game_view.h | 6 +- cockatrice/src/game/zones/select_zone.cpp | 3 +- .../widgets/dialogs/dlg_settings.cpp | 14 +- .../interface/widgets/dialogs/dlg_settings.h | 2 + 9 files changed, 170 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 1d8121b19..a66897b4a 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -284,6 +284,9 @@ SettingsCache::SettingsCache() closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); + showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); + showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); + showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); displayCardNames = settings->value("cards/displaycardnames", true).toBool(); @@ -1308,6 +1311,18 @@ void SettingsCache::setRoundCardCorners(bool _roundCardCorners) emit roundCardCornersChanged(roundCardCorners); } +void SettingsCache::setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount) +{ + showDragSelectionCount = static_cast(_showDragSelectionCount); + settings->setValue("interface/showlassoselectioncount", showDragSelectionCount); +} + +void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount) +{ + showTotalSelectionCount = static_cast(_showTotalSelectionCount); + settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); +} + void SettingsCache::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 2bbf85352..ece61487f 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -340,6 +340,8 @@ private: bool isPortableBuild; bool roundCardCorners; bool showStatusBar; + bool showDragSelectionCount; + bool showTotalSelectionCount; public: SettingsCache(); @@ -455,6 +457,14 @@ public: { return showStatusBar; } + [[nodiscard]] bool getShowDragSelectionCount() const + { + return showDragSelectionCount; + } + [[nodiscard]] bool getShowTotalSelectionCount() const + { + return showTotalSelectionCount; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -1120,5 +1130,7 @@ public slots: void setUpdateReleaseChannelIndex(int value); void setMaxFontSize(int _max); void setRoundCardCorners(bool _roundCardCorners); + void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); + void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); }; #endif diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 5dc3b48f7..034ff6947 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -521,9 +521,9 @@ void GameScene::startRubberBand(const QPointF &selectionOrigin) emit sigStartRubberBand(selectionOrigin); } -void GameScene::resizeRubberBand(const QPointF &cursorPoint) +void GameScene::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) { - emit sigResizeRubberBand(cursorPoint); + emit sigResizeRubberBand(cursorPoint, selectedCount); } void GameScene::stopRubberBand() diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 86fa0795a..f08e83aa4 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -163,7 +163,7 @@ public: /** Unregisters a card from animation updates. */ void unregisterAnimationItem(AbstractCardItem *card); void startRubberBand(const QPointF &selectionOrigin); - void resizeRubberBand(const QPointF &cursorPoint); + void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); public slots: @@ -196,7 +196,7 @@ protected: signals: void sigStartRubberBand(const QPointF &selectionOrigin); - void sigResizeRubberBand(const QPointF &cursorPoint); + void sigResizeRubberBand(const QPointF &cursorPoint, int selectedCount); void sigStopRubberBand(); }; diff --git a/cockatrice/src/game/game_view.cpp b/cockatrice/src/game/game_view.cpp index dd5cc70c1..ce53828a7 100644 --- a/cockatrice/src/game/game_view.cpp +++ b/cockatrice/src/game/game_view.cpp @@ -4,9 +4,32 @@ #include "game_scene.h" #include +#include #include #include +// QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. +// This subclass disables that behavior so dragCountLabel can appear above it. +class SelectionRubberBand : public QRubberBand +{ +public: + using QRubberBand::QRubberBand; + +protected: + void showEvent(QShowEvent *event) override + { + QWidget::showEvent(event); // Skip QRubberBand's raise() + } + + void changeEvent(QEvent *event) override + { + if (event->type() == QEvent::ZOrderChange) { + return; // Skip QRubberBand's raise() on z-order changes + } + QRubberBand::changeEvent(event); + } +}; + GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, parent), rubberBand(0) { setBackgroundBrush(QBrush(QColor(0, 0, 0))); @@ -19,6 +42,7 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(scene, &GameScene::sigStartRubberBand, this, &GameView::startRubberBand); connect(scene, &GameScene::sigResizeRubberBand, this, &GameView::resizeRubberBand); connect(scene, &GameScene::sigStopRubberBand, this, &GameView::stopRubberBand); + connect(scene, &QGraphicsScene::selectionChanged, this, [this]() { updateTotalSelectionCount(); }); aCloseMostRecentZoneView = new QAction(this); @@ -27,7 +51,23 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this, &GameView::refreshShortcuts); refreshShortcuts(); - rubberBand = new QRubberBand(QRubberBand::Rectangle, this); + rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); + + const QString countLabelStyle = "color: white; " + "font-size: 14px; " + "font-weight: bold; " + "background-color: rgba(0, 0, 0, 160); " + "border-radius: 3px; " + "padding: 1px 2px;"; + + dragCountLabel = new QLabel(this); + dragCountLabel->setStyleSheet(countLabelStyle); + dragCountLabel->hide(); + dragCountLabel->raise(); + + totalCountLabel = new QLabel(this); + totalCountLabel->setStyleSheet(countLabelStyle); + totalCountLabel->hide(); } void GameView::resizeEvent(QResizeEvent *event) @@ -39,6 +79,7 @@ void GameView::resizeEvent(QResizeEvent *event) s->processViewSizeChange(event->size()); updateSceneRect(scene()->sceneRect()); + updateTotalSelectionCount(event->size()); } void GameView::updateSceneRect(const QRectF &rect) @@ -48,20 +89,67 @@ void GameView::updateSceneRect(const QRectF &rect) void GameView::startRubberBand(const QPointF &_selectionOrigin) { + if (!rubberBand) + return; + selectionOrigin = _selectionOrigin; rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0))); rubberBand->show(); } -void GameView::resizeRubberBand(const QPointF &cursorPoint) +void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount) { - if (rubberBand) - rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), cursorPoint.toPoint()).normalized()); + if (!rubberBand) + return; + + constexpr int kLabelPaddingInPixels = 4; + + QPoint cursor = cursorPoint.toPoint(); + QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized(); + rubberBand->setGeometry(rect); + + if (!SettingsCache::instance().getShowDragSelectionCount()) { + dragCountLabel->hide(); + return; + } + + if (selectedCount > 0) { + dragCountLabel->setText(QString::number(selectedCount)); + dragCountLabel->adjustSize(); + QSize labelSize = dragCountLabel->size(); + + if (rect.width() < labelSize.width() + 2 * kLabelPaddingInPixels || + rect.height() < labelSize.height() + 2 * kLabelPaddingInPixels) { + dragCountLabel->hide(); + return; + } + + const int minX = rect.left() + kLabelPaddingInPixels; + const int minY = rect.top() + kLabelPaddingInPixels; + + int x = qMax(minX, cursor.x() - labelSize.width() - kLabelPaddingInPixels); + int y = qMax(minY, cursor.y() - labelSize.height() - kLabelPaddingInPixels); + + bool isAtTopLeftCorner = (x == minX) && (y == minY); + if (isAtTopLeftCorner) { + constexpr int kCursorClearanceInPixels = 16; + x = qMin(cursor.x() + kCursorClearanceInPixels, rect.right() - labelSize.width() - kLabelPaddingInPixels); + } + + dragCountLabel->move(x, y); + dragCountLabel->show(); + } else { + dragCountLabel->hide(); + } } void GameView::stopRubberBand() { + if (!rubberBand) + return; + rubberBand->hide(); + dragCountLabel->hide(); } void GameView::refreshShortcuts() @@ -69,3 +157,28 @@ void GameView::refreshShortcuts() aCloseMostRecentZoneView->setShortcuts( SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); } + +void GameView::updateTotalSelectionCount(const QSize &viewSize) +{ + if (!SettingsCache::instance().getShowTotalSelectionCount()) { + totalCountLabel->hide(); + return; + } + + int count = scene()->selectedItems().count(); + + if (count > 1) { + totalCountLabel->setText(QString::number(count)); + totalCountLabel->adjustSize(); + + constexpr int kMarginInPixels = 10; + int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); + int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); + int x = availableWidth - totalCountLabel->width() - kMarginInPixels; + int y = availableHeight - totalCountLabel->height() - kMarginInPixels; + totalCountLabel->move(x, y); + totalCountLabel->show(); + } else { + totalCountLabel->hide(); + } +} diff --git a/cockatrice/src/game/game_view.h b/cockatrice/src/game/game_view.h index 72df9cd08..a77ab9257 100644 --- a/cockatrice/src/game/game_view.h +++ b/cockatrice/src/game/game_view.h @@ -10,6 +10,7 @@ #include class GameScene; +class QLabel; class QRubberBand; class GameView : public QGraphicsView @@ -18,15 +19,18 @@ class GameView : public QGraphicsView private: QAction *aCloseMostRecentZoneView; QRubberBand *rubberBand; + QLabel *dragCountLabel; + QLabel *totalCountLabel; QPointF selectionOrigin; protected: void resizeEvent(QResizeEvent *event) override; private slots: void startRubberBand(const QPointF &selectionOrigin); - void resizeRubberBand(const QPointF &cursorPoint); + void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); void refreshShortcuts(); + void updateTotalSelectionCount(const QSize &viewSize = QSize()); public slots: void updateSceneRect(const QRectF &rect); diff --git a/cockatrice/src/game/zones/select_zone.cpp b/cockatrice/src/game/zones/select_zone.cpp index 719eec148..9bf5f9faf 100644 --- a/cockatrice/src/game/zones/select_zone.cpp +++ b/cockatrice/src/game/zones/select_zone.cpp @@ -68,7 +68,8 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) } } static_cast(scene())->resizeRubberBand( - deviceTransform(static_cast(scene())->getViewportTransform()).map(pos)); + deviceTransform(static_cast(scene())->getViewportTransform()).map(pos), + cardsInSelectionRect.size()); event->accept(); } } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp index e7286f078..e3bf209dc 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -821,6 +821,14 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&annotateTokensCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setAnnotateTokens); + showDragSelectionCountCheckBox.setChecked(SettingsCache::instance().getShowDragSelectionCount()); + connect(&showDragSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowDragSelectionCount); + + showTotalSelectionCountCheckBox.setChecked(SettingsCache::instance().getShowTotalSelectionCount()); + connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowTotalSelectionCount); + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); @@ -833,7 +841,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&closeEmptyCardViewCheckBox, 4, 0); generalGrid->addWidget(&focusCardViewSearchBarCheckBox, 5, 0); generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); - generalGrid->addWidget(&useTearOffMenusCheckBox, 7, 0); + generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); + generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); + generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -955,6 +965,8 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); + showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); + showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); notificationsGroupBox->setTitle(tr("Notifications settings")); notificationsEnabledCheckBox.setText(tr("Enable notifications in taskbar")); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h index db107c6e2..b655a30bc 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -171,6 +171,8 @@ private: QCheckBox closeEmptyCardViewCheckBox; QCheckBox focusCardViewSearchBarCheckBox; QCheckBox annotateTokensCheckBox; + QCheckBox showDragSelectionCountCheckBox; + QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox;