From 8b20fa042508426dac126deae276ff9468ef3f5c Mon Sep 17 00:00:00 2001 From: DawnFire42 Date: Sat, 14 Mar 2026 00:10:45 -0400 Subject: [PATCH] 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. --- .../src/client/settings/cache_settings.cpp | 8 ++ .../src/client/settings/cache_settings.h | 6 ++ cockatrice/src/game/game_scene.cpp | 4 +- cockatrice/src/game/game_scene.h | 4 +- cockatrice/src/game/game_view.cpp | 90 ++++++++++++++++++- cockatrice/src/game/game_view.h | 4 +- cockatrice/src/game/zones/select_zone.cpp | 3 +- .../widgets/dialogs/dlg_settings.cpp | 8 +- .../interface/widgets/dialogs/dlg_settings.h | 1 + 9 files changed, 117 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 1d8121b19..bd1f1cd23 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -284,6 +284,8 @@ SettingsCache::SettingsCache() closeEmptyCardView = settings->value("interface/closeEmptyCardView", true).toBool(); focusCardViewSearchBar = settings->value("interface/focusCardViewSearchBar", true).toBool(); + showDragSelectionCount = settings->value("interface/showlassoselectioncount", 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 +1310,12 @@ 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::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 2bbf85352..b8b68cda4 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -340,6 +340,7 @@ private: bool isPortableBuild; bool roundCardCorners; bool showStatusBar; + bool showDragSelectionCount; public: SettingsCache(); @@ -455,6 +456,10 @@ public: { return showStatusBar; } + [[nodiscard]] bool getShowDragSelectionCount() const + { + return showDragSelectionCount; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -1120,5 +1125,6 @@ public slots: void setUpdateReleaseChannelIndex(int value); void setMaxFontSize(int _max); void setRoundCardCorners(bool _roundCardCorners); + void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); }; #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..d9aee7d16 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))); @@ -27,7 +50,19 @@ 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(); } void GameView::resizeEvent(QResizeEvent *event) @@ -48,20 +83,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() diff --git a/cockatrice/src/game/game_view.h b/cockatrice/src/game/game_view.h index 72df9cd08..3c1b1467b 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,13 +19,14 @@ class GameView : public QGraphicsView private: QAction *aCloseMostRecentZoneView; QRubberBand *rubberBand; + QLabel *dragCountLabel; 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(); public slots: 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..851230881 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.cpp @@ -821,6 +821,10 @@ 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); + 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 +837,8 @@ 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(&useTearOffMenusCheckBox, 8, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -955,6 +960,7 @@ 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")); 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..1bcbbbdf2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_settings.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_settings.h @@ -171,6 +171,7 @@ private: QCheckBox closeEmptyCardViewCheckBox; QCheckBox focusCardViewSearchBarCheckBox; QCheckBox annotateTokensCheckBox; + QCheckBox showDragSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox;