diff --git a/cockatrice/src/game_graphics/board/arrow_item.cpp b/cockatrice/src/game_graphics/board/arrow_item.cpp index ce8967bb5..af63d047d 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.cpp +++ b/cockatrice/src/game_graphics/board/arrow_item.cpp @@ -4,12 +4,14 @@ #include "../../client/settings/cache_settings.h" #include "../../game/player/player_actions.h" #include "../../game/player/player_logic.h" +#include "../game_scene.h" #include "../player/player_target.h" #include "../z_values.h" #include "../zones/card_zone.h" #include "card_item.h" #include +#include #include #include #include @@ -18,10 +20,27 @@ #include #include #include +#include #include #include #include +namespace +{ +constexpr qreal kMinStrokeDurationMs = 200.0; +constexpr qreal kMaxStrokeDurationMs = 450.0; +constexpr qreal kMsPerPixel = 0.8; +constexpr qreal kGlowFadeDurationMs = 120.0; +constexpr qreal kSheenHalfWidth = 14.0; + +/// @brief Ease-out cubic, for a natural "slow in / slow out" reveal. +qreal easeOutCubic(qreal t) +{ + const qreal inverse = 1.0 - t; + return 1.0 - inverse * inverse * inverse; +} +} // namespace + ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) { @@ -47,6 +66,13 @@ ArrowItem::ArrowItem(QSharedPointer _data, ArrowTarget *_startI } } +ArrowItem::~ArrowItem() +{ + if (auto *scene = qobject_cast(this->scene())) { + scene->unregisterAnimationItem(this); + } +} + void ArrowItem::onTargetDestroyed() { emit requestDeletion(data->creatorId, data->id); @@ -91,16 +117,21 @@ void ArrowItem::updatePath(const QPointF &endPoint) prepareGeometryChange(); if (lineLength < 30) { path = QPainterPath(); + bodyPath = QPainterPath(); + headPath = QPainterPath(); + shaftOutlinePath = QPainterPath(); + centerLine = QPainterPath(); + headBaseFraction = 1.0; } else { QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength); - QPainterPath centerLine; + centerLine = QPainterPath(); centerLine.moveTo(0, 0); centerLine.quadTo(c, QPointF(lineLength, 0)); - double percentage = 1 - headLength / lineLength; - QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage); - QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001)); + headBaseFraction = 1 - headLength / lineLength; + QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction); + QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001)); qreal alpha = testLine.angle() - 90; QPointF endPoint1 = arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180)); @@ -111,20 +142,89 @@ void ArrowItem::updatePath(const QPointF &endPoint) QPointF point2 = endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180)); - path = QPainterPath(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); + QPointF start1 = -arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)); + QPointF start2 = arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180)); + + path = QPainterPath(start1); path.quadTo(c, endPoint1); path.lineTo(point1); path.lineTo(QPointF(lineLength, 0)); path.lineTo(point2); path.lineTo(endPoint2); - path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); - path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); + path.quadTo(c, start2); + path.lineTo(start1); + + bodyPath = QPainterPath(start1); + bodyPath.quadTo(c, endPoint1); + bodyPath.lineTo(endPoint2); + bodyPath.quadTo(c, start2); + bodyPath.lineTo(start1); + + headPath = QPainterPath(endPoint1); + headPath.lineTo(point1); + headPath.lineTo(QPointF(lineLength, 0)); + headPath.lineTo(point2); + headPath.lineTo(endPoint2); + + shaftOutlinePath = QPainterPath(start1); + shaftOutlinePath.quadTo(c, endPoint1); + shaftOutlinePath.moveTo(endPoint2); + shaftOutlinePath.quadTo(c, start2); + shaftOutlinePath.lineTo(start1); } setPos(startPoint); setTransform(QTransform().rotate(-line.angle())); } +void ArrowItem::startDrawAnimation() +{ + if (!SettingsCache::instance().cardsDisplay().getArrowDrawAnimation() || centerLine.isEmpty()) { + return; + } + + strokeDurationMs = qBound(kMinStrokeDurationMs, centerLine.length() * kMsPerPixel, kMaxStrokeDurationMs); + glowFadeDurationMs = kGlowFadeDurationMs; + // The clock is started on the first animationEvent() tick so that t=0 + // corresponds to the first rendered frame. Starting it here would count + // the time spent before the item's first paint (event-loop delays, bursts + // of arrows created together), making the arrow appear already partway + // drawn when it first shows up. + animationStarted = false; + drawProgress = 0.0; + glowAlpha = 1.0; + update(); + if (auto *scene = qobject_cast(this->scene())) { + scene->registerAnimationItem(this); + } +} + +bool ArrowItem::animationEvent() +{ + if (!animationStarted) { + animationClock.start(); + animationStarted = true; + } + + const qint64 elapsed = animationClock.elapsed(); + if (elapsed >= strokeDurationMs + glowFadeDurationMs) { + drawProgress = 1.0; + glowAlpha = 0.0; + update(); + return false; + } + + if (elapsed < strokeDurationMs) { + drawProgress = easeOutCubic(qBound(0.0, elapsed / strokeDurationMs, 1.0)); + glowAlpha = 1.0; + } else { + drawProgress = 1.0; + glowAlpha = 1.0 - (elapsed - strokeDurationMs) / glowFadeDurationMs; + } + update(); + return true; +} + void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) { QColor paintColor(data->color); @@ -133,8 +233,66 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti } else { paintColor.setAlpha(150); } + + painter->save(); + const QPen outlinePen = painter->pen(); painter->setBrush(paintColor); - painter->drawPath(path); + + const auto drawShaft = [this, painter, &outlinePen, paintColor]() { + painter->setPen(Qt::NoPen); + painter->drawPath(bodyPath); + painter->setPen(outlinePen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(shaftOutlinePath); + painter->setBrush(paintColor); + }; + + if (drawProgress >= 1.0 || path.isEmpty()) { + painter->drawPath(path); + } else if (drawProgress < headBaseFraction) { + // The reveal edge and the sheen share the same arc-length parameterization, + // so the stroke stays exactly in sync with the trailing sheen. + const qreal revealX = centerLine.pointAtPercent(drawProgress).x(); + QPainterPath clip; + clip.addRect(QRectF(-glowExtent, path.boundingRect().top() - glowExtent, revealX + glowExtent, + path.boundingRect().height() + 2 * glowExtent)); + painter->setClipPath(clip); + drawShaft(); + } else { + // Once the reveal reaches the head base, pop the whole head in with a fade + // instead of slicing the triangle into a growing stub. + drawShaft(); + const qreal headFadeIn = (drawProgress - headBaseFraction) / (1.0 - headBaseFraction); + painter->setOpacity(headFadeIn); + painter->setPen(Qt::NoPen); + painter->drawPath(headPath); + painter->setPen(outlinePen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(headPath); + painter->setOpacity(1.0); + painter->setBrush(paintColor); + } + + if (glowAlpha > 0.0 && !centerLine.isEmpty()) { + // Sweep a bright band across the arrow. Clipping to the + // silhouette keeps it flat against the shaft so it reads as a light reflection. + const qreal anticipation = qMin(1.0, drawProgress / 0.08); + const QPointF sweep = centerLine.pointAtPercent(qMin(drawProgress, 1.0)); + QLinearGradient sheen(sweep.x() - kSheenHalfWidth, 0.0, sweep.x() + kSheenHalfWidth, 0.0); + sheen.setColorAt(0.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0)); + sheen.setColorAt(0.5, QColor(255, 255, 255, 200)); + sheen.setColorAt(1.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0)); + painter->save(); + painter->setPen(Qt::NoPen); + painter->setClipPath(path); + painter->setBrush(sheen); + painter->setOpacity(glowAlpha * anticipation); + painter->drawRect(QRectF(sweep.x() - kSheenHalfWidth - glowExtent, path.boundingRect().top() - glowExtent, + (kSheenHalfWidth + glowExtent) * 2.0, + path.boundingRect().height() + glowExtent * 2.0)); + painter->restore(); + } + painter->restore(); } void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) diff --git a/cockatrice/src/game_graphics/board/arrow_item.h b/cockatrice/src/game_graphics/board/arrow_item.h index 1c306e065..21f991b77 100644 --- a/cockatrice/src/game_graphics/board/arrow_item.h +++ b/cockatrice/src/game_graphics/board/arrow_item.h @@ -2,9 +2,12 @@ #define ARROWITEM_H #include "../../game/board/arrow_data.h" +#include "../animated_item.h" #include "arrow_target.h" +#include #include +#include #include #include @@ -12,7 +15,7 @@ class CardItem; class QGraphicsSceneMouseEvent; class PlayerLogic; -class ArrowItem : public QObject, public QGraphicsItem +class ArrowItem : public QObject, public QGraphicsItem, public IAnimatedItem { Q_OBJECT Q_INTERFACES(QGraphicsItem) @@ -21,6 +24,19 @@ signals: private: QPainterPath path; + QPainterPath bodyPath; + QPainterPath headPath; + QPainterPath shaftOutlinePath; + QPainterPath centerLine; + qreal headBaseFraction = 1.0; + QElapsedTimer animationClock; + qreal strokeDurationMs = 0; + qreal glowFadeDurationMs = 0; + qreal drawProgress = 1.0; + qreal glowAlpha = 0.0; + bool animationStarted = false; + + static constexpr qreal glowExtent = 12.0; protected: QSharedPointer data; @@ -33,16 +49,19 @@ protected: public: ArrowItem(QSharedPointer _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); + ~ArrowItem() override; void onTargetDestroyed(); void delArrow(); void updatePath(); void updatePath(const QPointF &endPoint); + void startDrawAnimation(); + bool animationEvent() override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; [[nodiscard]] QRectF boundingRect() const override { - return path.boundingRect(); + return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent); } [[nodiscard]] QPainterPath shape() const override { @@ -106,4 +125,4 @@ protected: void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; }; -#endif \ No newline at end of file +#endif diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index 25c5fbcf0..4d3144ad4 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -502,6 +502,7 @@ void GameScene::addArrow(QSharedPointer data) auto *arrow = new ArrowItem(data, startCard, targetItem); addItem(arrow); + arrow->startDrawAnimation(); arrowRegistry.insert(data, arrow); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); } diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 3fa56dd48..a20d31652 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -116,6 +116,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::setTapAnimation); + arrowDrawAnimationCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getArrowDrawAnimation()); + connect(&arrowDrawAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), + &CardsDisplaySettings::setArrowDrawAnimation); + lifeCounterAnimationsCheckBox.setChecked( SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()); connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), @@ -132,8 +136,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() animationGrid->addWidget(&enableAllAnimationsButton, 0, 0); animationGrid->addWidget(&disableAllAnimationsButton, 0, 1); animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); - animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 2, 0); - animationGrid->addWidget(&battlefieldFlashCheckBox, 3, 0); + animationGrid->addWidget(&arrowDrawAnimationCheckBox, 2, 0); + animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 3, 0); + animationGrid->addWidget(&battlefieldFlashCheckBox, 4, 0); animationGroupBox = new QGroupBox; animationGroupBox->setLayout(animationGrid); @@ -287,6 +292,7 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) void UserInterfaceSettingsPage::enableAllAnimations() { tapAnimationCheckBox.setChecked(true); + arrowDrawAnimationCheckBox.setChecked(true); lifeCounterAnimationsCheckBox.setChecked(true); battlefieldFlashCheckBox.setChecked(true); } @@ -294,6 +300,7 @@ void UserInterfaceSettingsPage::enableAllAnimations() void UserInterfaceSettingsPage::disableAllAnimations() { tapAnimationCheckBox.setChecked(false); + arrowDrawAnimationCheckBox.setChecked(false); lifeCounterAnimationsCheckBox.setChecked(false); battlefieldFlashCheckBox.setChecked(false); } @@ -343,6 +350,7 @@ void UserInterfaceSettingsPage::retranslateUi() enableAllAnimationsButton.setText(tr("&Enable all animations")); disableAllAnimationsButton.setText(tr("&Disable all animations")); tapAnimationCheckBox.setText(tr("&Tap/untap animation")); + arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation")); lifeCounterAnimationsCheckBox.setText(tr("Life counter flash")); battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage")); deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index f18ab8ccf..2b9eba72c 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -40,6 +40,7 @@ private: QPushButton enableAllAnimationsButton; QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; + QCheckBox arrowDrawAnimationCheckBox; QCheckBox lifeCounterAnimationsCheckBox; QCheckBox battlefieldFlashCheckBox; QCheckBox openDeckInNewTabCheckBox; diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp index 0bc927eeb..c15f614d8 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -67,9 +66,6 @@ void TabDeckEditorVisual::createCentralFrame() centralFrame = new QVBoxLayout; centralWidget->setLayout(centralFrame); - auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this); - databaseModel->setObjectName("databaseModel"); - tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel); connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, diff --git a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp index 2ee560859..5ccfcc28f 100644 --- a/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.cpp @@ -25,7 +25,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent, layout = new QVBoxLayout(this); setLayout(layout); - visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel()); + visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel(), + _cardDatabaseModel); visualDeckView->setObjectName("visualDeckView"); connect(visualDeckView, &VisualDeckEditorWidget::activeCardChanged, this, &TabDeckEditorVisualTabWidget::onCardChanged); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 62e1bf5ba..4a558a5e0 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -82,17 +82,14 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg void VisualDatabaseDisplayFilterToolbarWidget::initialize() { - // create groupbox layouts - auto sortLayout = new QHBoxLayout(this); + auto sortLayout = new QHBoxLayout(sortGroupBox); sortLayout->setContentsMargins(0, 0, 0, 0); sortLayout->setSpacing(0); - sortGroupBox->setLayout(sortLayout); sortLayout->setAlignment(Qt::AlignLeft); - auto filterLayout = new QHBoxLayout(this); + auto filterLayout = new QHBoxLayout(filterGroupBox); filterLayout->setContentsMargins(0, 0, 0, 0); filterLayout->setSpacing(2); - filterGroupBox->setLayout(filterLayout); filterLayout->setAlignment(Qt::AlignLeft); // create settings widgets diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index dc98e6940..0cdf60d5d 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -140,9 +141,6 @@ void VisualDatabaseDisplayWidget::initialize() { databaseLoadIndicator->setVisible(false); - filterContainer->initialize(); - filterContainer->setVisible(true); - searchContainer->addWidget(colorFilterWidget); searchContainer->addWidget(clearFilterWidget); searchContainer->addWidget(searchEdit); @@ -158,17 +156,43 @@ void VisualDatabaseDisplayWidget::initialize() mainLayout->addWidget(cardSizeWidget); - databaseDisplayModel->setFilterTree(filterModel->filterTree()); - connect(filterModel, &FilterTreeModel::layoutChanged, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); - loadCardsTimer = new QTimer(this); - loadCardsTimer->setSingleShot(true); // Ensure it only fires once after the timeout + initializeFilters(); +} - connect(loadCardsTimer, &QTimer::timeout, this, [this]() { loadCurrentPage(); }); - loadCardsTimer->start(5000); +void VisualDatabaseDisplayWidget::initializeFilters() +{ + if (filtersInitialized || !isVisible() || CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { + return; + } - retranslateUi(); + filtersInitialized = true; + + // The filter toolbar builds its widgets by iterating the entire card database + // (per-set, per-main-type, per-sub-type and per-format buttons). Building it + // inside showEvent would block the tab switch, so keep it hidden and defer the + // build to the next event loop turn, letting the tab paint first. The toolbar + // then appears one event loop turn later, shifting the grid down by the toolbar + // height -- the intended tradeoff of an responsive tab switch. + filterContainer->setVisible(false); + + QTimer::singleShot(0, this, [this] { + filterContainer->initialize(); + filterContainer->setVisible(true); + + databaseDisplayModel->setFilterTree(filterModel->filterTree()); + + QTimer::singleShot(5000, this, [this] { loadCurrentPage(); }); + + retranslateUi(); + }); +} + +void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + initializeFilters(); } void VisualDatabaseDisplayWidget::retranslateUi() @@ -292,9 +316,17 @@ void VisualDatabaseDisplayWidget::loadCurrentPage() { // Ensure only the initial page is loaded if (currentPage == 0) { - // Only load the first page initially - qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; - populateCards(); + if (!initialLoadScheduled) { + initialLoadScheduled = true; + qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; + // Defer the first page so the tab switch stays responsive. The card + // grid builds one event loop turn later. This also applies to + // search-driven reloads, which reset currentPage back to 0. + QTimer::singleShot(0, this, [this] { + initialLoadScheduled = false; + populateCards(); + }); + } } else if (nearEndOfPage()) { // If not the first page, just load the next page and append to the flow widget loadNextPage(); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index a383e8ead..6e4d87876 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -115,17 +115,20 @@ private: OverlapControlWidget *overlapControlWidget; CardSizeWidget *cardSizeWidget; QTimer *debounceTimer; - QTimer *loadCardsTimer; int debounceTime = 300; // in Ms int currentPage = 0; // Current page index int cardsPerPage = 100; // Number of cards per page + bool filtersInitialized = false; + bool initialLoadScheduled = false; + void initializeFilters(); void highlightAllSearchEdit(); bool nearEndOfPage() const; protected: void resizeEvent(QResizeEvent *event) override; + void showEvent(QShowEvent *event) override; }; #endif // VISUAL_DATABASE_DISPLAY_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index 6a4eaa382..e3261b346 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -29,8 +29,10 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, DeckListModel *_deckListModel, - QItemSelectionModel *_selectionModel) - : QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel) + QItemSelectionModel *_selectionModel, + CardDatabaseModel *_cardDatabaseModel) + : QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel), + cardDatabaseModel(_cardDatabaseModel) { // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -94,7 +96,9 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() setFocusProxy(searchBar); setFocusPolicy(Qt::ClickFocus); - cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + if (!cardDatabaseModel) { + cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); + } cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h index da02b5c1f..ac0d07efd 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -38,7 +38,10 @@ class VisualDeckEditorWidget : public QWidget Q_OBJECT public: - explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel); + explicit VisualDeckEditorWidget(QWidget *parent, + DeckListModel *deckListModel, + QItemSelectionModel *selectionModel, + CardDatabaseModel *_cardDatabaseModel = nullptr); void retranslateUi(); void updateCompactMode(); void clearAllDisplayWidgets(); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index 174943333..5b9c5a4b5 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -12,6 +12,17 @@ CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent, const ICardPreferenceProvider *prefs) : QObject(_parent), db(_db), prefs(prefs) { + // Invalidate the cached count maps whenever the database contents change. + connect(db, &CardDatabase::cardAdded, this, &CardDatabaseQuerier::invalidateCaches); + connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseQuerier::invalidateCaches); + connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseQuerier::invalidateCaches); +} + +void CardDatabaseQuerier::invalidateCaches() +{ + mainCardTypeCountsCache.clear(); + subCardTypeCountsCache.clear(); + formatsCountCache.clear(); } /** @@ -304,44 +315,43 @@ QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardN QStringList CardDatabaseQuerier::getAllMainCardTypes() const { - QSet types; - for (const auto &card : db->cards.values()) { - types.insert(card->getMainCardType()); - } - return types.values(); + return getAllMainCardTypesWithCount().keys(); } QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const { - QMap typeCounts; - - for (const auto &card : db->cards.values()) { - QString type = card->getMainCardType(); - typeCounts[type]++; + // An empty cache is always recomputed correctly: a database with no cards + // produces an empty map, so the cache is only ever empty when it needs a + // (trivially cheap) rebuild. + if (mainCardTypeCountsCache.isEmpty()) { + for (const auto &card : db->cards.values()) { + QString type = card->getMainCardType(); + mainCardTypeCountsCache[type]++; + } } - return typeCounts; + return mainCardTypeCountsCache; } QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const { - QMap typeCounts; + if (subCardTypeCountsCache.isEmpty()) { + for (const auto &card : db->cards.values()) { + QString type = card->getCardType(); - for (const auto &card : db->cards.values()) { - QString type = card->getCardType(); + QStringList parts = type.split(" — "); - QStringList parts = type.split(" — "); + if (parts.size() > 1) { // Ensure there are subtypes + QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); - if (parts.size() > 1) { // Ensure there are subtypes - QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); - - for (const QString &subtype : subtypes) { - typeCounts[subtype]++; + for (const QString &subtype : subtypes) { + subCardTypeCountsCache[subtype]++; + } } } } - return typeCounts; + return subCardTypeCountsCache; } FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const @@ -351,18 +361,18 @@ FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const QMap CardDatabaseQuerier::getAllFormatsWithCount() const { - QMap formatCounts; + if (formatsCountCache.isEmpty()) { + for (const auto &card : db->cards.values()) { + QStringList allProps = card->getProperties(); - for (const auto &card : db->cards.values()) { - QStringList allProps = card->getProperties(); - - for (const QString &prop : allProps) { - if (prop.startsWith("format-")) { - QString formatName = prop.mid(QStringLiteral("format-").size()); - formatCounts[formatName]++; + for (const QString &prop : allProps) { + if (prop.startsWith("format-")) { + QString formatName = prop.mid(QStringLiteral("format-").size()); + formatsCountCache[formatName]++; + } } } } - return formatCounts; + return formatsCountCache; } diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h index ff8d7958b..f195a8170 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h @@ -220,6 +220,16 @@ public: private: const CardDatabase *db; //!< Card database used for all lookups. const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings. + + // Count maps are expensive to compute (they iterate the whole database) and are + // queried every time a filter widget is built, so cache them and invalidate on + // any database mutation. Only the main thread reads or writes these. + mutable QMap mainCardTypeCountsCache; + mutable QMap subCardTypeCountsCache; + mutable QMap formatsCountCache; + +private slots: + void invalidateCaches(); }; #endif // COCKATRICE_CARD_DATABASE_QUERIER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h index 3ee3d2aef..3f2cbbe8e 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -15,6 +15,7 @@ public: [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0; + [[nodiscard]] virtual bool getArrowDrawAnimation() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp index 7b248a982..ecd26a9f8 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -111,19 +111,50 @@ bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card void CardDatabaseModel::cardDatabaseEnabledSetsChanged() { - // remove all the cards no more present in at least one enabled set + // Build the new card list in a single pass. + QList newCardList; + newCardList.reserve(cardList.size()); for (const CardInfoPtr &card : cardList) { - if (!checkCardHasAtLeastOneEnabledSet(card)) { - cardRemoved(card); + if (checkCardHasAtLeastOneEnabledSet(card)) { + newCardList.append(card); + } + } + for (const CardInfoPtr &card : db->getCardList()) { + if (!cardListSet.contains(card) && checkCardHasAtLeastOneEnabledSet(card)) { + newCardList.append(card); } } - // re-check all the card currently not shown, maybe their part of a newly-enabled set - for (const CardInfoPtr &card : db->getCardList()) { - if (!cardListSet.contains(card)) { - cardAdded(card); + if (newCardList == cardList) { + return; + } + + // Rebuild the whole list inside a single model reset instead of emitting + // per-card insert/remove notifications. With tens of thousands of cards the + // per-card path is the dominant cost of constructing a CardDatabaseModel. + QSet oldCardListSet = cardListSet; + QSet newCardListSet(newCardList.begin(), newCardList.end()); + + beginResetModel(); + + // Disconnect cards that are no longer shown. + for (const CardInfoPtr &card : cardList) { + if (!newCardListSet.contains(card)) { + disconnect(card.data(), nullptr, this, nullptr); } } + + cardList = newCardList; + cardListSet = newCardListSet; + + // Connect cards that are now shown for the first time. + for (const CardInfoPtr &card : cardList) { + if (!oldCardListSet.contains(card)) { + connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged); + } + } + + endResetModel(); } void CardDatabaseModel::cardAdded(const CardInfoPtr &card) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 5b893799f..ba6ac4691 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -133,6 +133,22 @@ void Server_ProtocolHandler::sendProtocolItem(const RoomEvent &item) Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(const CommandContainer &cont, ResponseContainer &rc) { + const auto isPreAuthSessionCommand = [](SessionCommand::SessionCommandType type) { + switch (type) { + case SessionCommand::PING: + case SessionCommand::LOGIN: + case SessionCommand::REGISTER: + case SessionCommand::ACTIVATE: + case SessionCommand::FORGOT_PASSWORD_REQUEST: + case SessionCommand::FORGOT_PASSWORD_RESET: + case SessionCommand::FORGOT_PASSWORD_CHALLENGE: + case SessionCommand::REQUEST_PASSWORD_SALT: + return true; + default: + return false; + } + }; + Response::ResponseCode finalResponseCode = Response::RespOk; for (int i = cont.session_command_size() - 1; i >= 0; --i) { Response::ResponseCode resp = Response::RespInvalidCommand; @@ -141,33 +157,38 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co if (num != SessionCommand::PING) { // don't log ping commands logDebugMessage(getSafeDebugString(sc)); } - switch ((SessionCommand::SessionCommandType)num) { - case SessionCommand::PING: - resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); - break; - case SessionCommand::LOGIN: - resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); - break; - case SessionCommand::MESSAGE: - resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); - break; - case SessionCommand::GET_GAMES_OF_USER: - resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); - break; - case SessionCommand::GET_USER_INFO: - resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); - break; - case SessionCommand::LIST_ROOMS: - resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); - break; - case SessionCommand::JOIN_ROOM: - resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); - break; - case SessionCommand::LIST_USERS: - resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); - break; - default: - resp = processExtendedSessionCommand(num, sc, rc); + const auto commandType = static_cast(num); + if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) { + resp = Response::RespLoginNeeded; + } else { + switch (commandType) { + case SessionCommand::PING: + resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); + break; + case SessionCommand::LOGIN: + resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); + break; + case SessionCommand::MESSAGE: + resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); + break; + case SessionCommand::GET_GAMES_OF_USER: + resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); + break; + case SessionCommand::GET_USER_INFO: + resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); + break; + case SessionCommand::LIST_ROOMS: + resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); + break; + case SessionCommand::JOIN_ROOM: + resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); + break; + case SessionCommand::LIST_USERS: + resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); + break; + default: + resp = processExtendedSessionCommand(num, sc, rc); + } } if (resp != Response::RespOk) { finalResponseCode = resp; diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index 6ec1af962..f528a7c4b 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -50,6 +50,11 @@ bool CardsDisplaySettings::getTapAnimation() const return getValue("tapAnimation", QString(), QString(), true).toBool(); } +bool CardsDisplaySettings::getArrowDrawAnimation() const +{ + return getValue("arrowDrawAnimation", QString(), QString(), true).toBool(); +} + bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const { return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool(); @@ -159,6 +164,11 @@ void CardsDisplaySettings::setTapAnimation(bool _tapAnimation) setValue(_tapAnimation, "tapAnimation"); } +void CardsDisplaySettings::setArrowDrawAnimation(bool _arrowDrawAnimation) +{ + setValue(_arrowDrawAnimation, "arrowDrawAnimation"); +} + void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards) { setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards"); diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 15a3e3ff4..dbafa32ae 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -20,6 +20,7 @@ public: [[nodiscard]] bool getIncludeRebalancedCards() const override; [[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override; [[nodiscard]] bool getTapAnimation() const override; + [[nodiscard]] bool getArrowDrawAnimation() const override; [[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override; [[nodiscard]] bool getScaleCards() const override; [[nodiscard]] int getStackCardOverlapPercent() const override; @@ -40,6 +41,7 @@ public: void setIncludeRebalancedCards(bool _includeRebalancedCards); void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible); void setTapAnimation(bool _tapAnimation); + void setArrowDrawAnimation(bool _arrowDrawAnimation); void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards); void setCardScaling(bool _scaleCards); void setStackCardOverlapPercent(int _verticalCardOverlapPercent); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 842ddb4c8..d4a1b9217 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -896,6 +896,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Com Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer & /*rc*/) { + if (authState != PasswordRight) { + return Response::RespFunctionNotAllowed; + } + // code is of the form - QString code = QString::fromStdString(cmd.replay_code()); QStringList split = code.split("-"); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index dfdad4780..041a60d6f 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -476,6 +476,12 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default) ASSERT_EQ(s.getSampleHandSize(), 7); } +TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default) +{ + CardsDisplaySettings s(settingsPath, nullptr); + ASSERT_EQ(s.getArrowDrawAnimation(), true); +} + // --- VisualDeckStorageSettings --- TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)