diff --git a/cockatrice/src/game_graphics/animated_item.h b/cockatrice/src/game_graphics/animated_item.h new file mode 100644 index 000000000..700e0f62d --- /dev/null +++ b/cockatrice/src/game_graphics/animated_item.h @@ -0,0 +1,26 @@ +#ifndef ANIMATED_ITEM_H +#define ANIMATED_ITEM_H + +/** + * @file animated_item.h + * @ingroup GameGraphics + * @brief Interface for scene items driven by GameScene's shared animation timer. + * + * Items that want per-tick animation while a single QBasicTimer runs (instead of + * owning their own QTimer) implement this interface and register with the scene + * via GameScene::registerAnimationItem. + */ + +class IAnimatedItem +{ +public: + virtual ~IAnimatedItem() = default; + + /** + * @brief Advances the item's animation by one timer tick. + * @return true while the animation is still running, false once it has finished. + */ + virtual bool animationEvent() = 0; +}; + +#endif diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index e0029ee2d..1410d0c80 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -305,6 +305,11 @@ void AbstractCardItem::setTapped(bool _tapped, bool canAnimate) } } +bool AbstractCardItem::animationEvent() +{ + return false; +} + void AbstractCardItem::setFaceDown(bool _facedown) { facedown = _facedown; diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index bdb5f7cf1..8cbe95282 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -7,6 +7,7 @@ #ifndef ABSTRACTCARDITEM_H #define ABSTRACTCARDITEM_H +#include "../animated_item.h" #include "../card_dimensions.h" #include "arrow_target.h" #include "graphics_item_type.h" @@ -16,7 +17,7 @@ class PlayerLogic; -class AbstractCardItem : public ArrowTarget +class AbstractCardItem : public ArrowTarget, public IAnimatedItem { Q_OBJECT protected: @@ -126,6 +127,9 @@ public: emit deleteCardInfoPopup(cardRef.name); } + /** @brief Default: no per-tick animation. Subclasses override to animate. */ + bool animationEvent() override; + protected: void transformPainter(QPainter *painter, const QSizeF &translatedSize, int angle); void mousePressEvent(QGraphicsSceneMouseEvent *event) override; diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index 37f3bab50..2ba43d03d 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -137,7 +137,7 @@ public: void resetState(bool keepAnnotations = false); void processCardInfo(const ServerInfo_Card &_info); - bool animationEvent(); + bool animationEvent() override; CardDragItem *createDragItem(int _id, const QPointF &_pos, const QPointF &_scenePos, bool forceFaceDown); void deleteDragItem(); void drawArrow(const QColor &arrowColor); diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index db2088104..25c5fbcf0 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -45,7 +44,14 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent) GameScene::~GameScene() { + // Sever all incoming connections (animated item destroy-tracking) before the + // members below are destroyed: the base QGraphicsScene destructor destroys the + // remaining items, and their destroyed() signals must not reach slots that + // reference members that no longer exist. + disconnect(this); + delete animationTimer; + animationTimer = nullptr; // Delete all ArrowItems before QGraphicsScene's base destructor runs. // QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order. @@ -736,30 +742,45 @@ bool GameScene::event(QEvent *event) void GameScene::timerEvent(QTimerEvent * /*event*/) { - QMutableSetIterator i(cardsToAnimate); + QMutableHashIterator i(animatedItems); while (i.hasNext()) { i.next(); if (!i.value()->animationEvent()) { i.remove(); } } - if (cardsToAnimate.isEmpty()) { + if (animatedItems.isEmpty()) { animationTimer->stop(); } } -void GameScene::registerAnimationItem(AbstractCardItem *card) +void GameScene::registerAnimationItem(IAnimatedItem *item) { - cardsToAnimate.insert(static_cast(card)); - if (!animationTimer->isActive()) { + auto *object = dynamic_cast(item); + if (!object) { + return; + } + if (!animatedItems.contains(object)) { + connect(object, &QObject::destroyed, this, &GameScene::removeAnimatedItem); + } + animatedItems.insert(object, item); + if (animationTimer && !animationTimer->isActive()) { animationTimer->start(10, this); } } -void GameScene::unregisterAnimationItem(AbstractCardItem *card) +void GameScene::unregisterAnimationItem(IAnimatedItem *item) { - cardsToAnimate.remove(static_cast(card)); - if (cardsToAnimate.isEmpty()) { + animatedItems.remove(dynamic_cast(item)); + if (animationTimer && animatedItems.isEmpty()) { + animationTimer->stop(); + } +} + +void GameScene::removeAnimatedItem(QObject *item) +{ + animatedItems.remove(item); + if (animationTimer && animatedItems.isEmpty()) { animationTimer->stop(); } } diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index 74e979556..7f01bf1f5 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -4,13 +4,14 @@ #include "../game/arrow_registry.h" #include "../game/board/arrow_data.h" #include "../game/zones/card_zone_logic.h" +#include "animated_item.h" #include "board/arrow_item.h" #include +#include #include #include #include -#include inline Q_LOGGING_CATEGORY(GameSceneLog, "game_scene"); inline Q_LOGGING_CATEGORY(GameScenePlayerAdditionRemovalLog, "game_scene.player_addition_removal"); @@ -24,6 +25,7 @@ class CardItem; class ServerInfo_Card; class PhasesToolbar; class QBasicTimer; +class QObject; /** * @class GameScene @@ -50,8 +52,8 @@ private: QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size QPointer hoveredCard; ///< Currently hovered card - QBasicTimer *animationTimer; ///< Timer for card animations - QSet cardsToAnimate; ///< Cards currently animating + QBasicTimer *animationTimer; ///< Timer for scene animations + QHash animatedItems; ///< Items currently animating int playerRotation; ///< Rotation offset for player layout /** @@ -182,15 +184,24 @@ public: /** @brief Updates hovered card highlighting. */ void updateHoveredCard(CardItem *newCard); - /** @brief Registers a card for animation updates. */ - void registerAnimationItem(AbstractCardItem *card); + /** + * @brief Registers an item for animation updates with the shared scene timer. + * + * The item must inherit QObject; it is unregistered automatically when it is + * destroyed, so it may be deleted mid-animation without a dangling pointer. + */ + void registerAnimationItem(IAnimatedItem *item); - /** @brief Unregisters a card from animation updates. */ - void unregisterAnimationItem(AbstractCardItem *card); + /** @brief Unregisters an item from animation updates. */ + void unregisterAnimationItem(IAnimatedItem *item); void startRubberBand(const QPointF &selectionOrigin); void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void stopRubberBand(); +private slots: + /** @brief Removes a destroyed item from the animation set. */ + void removeAnimatedItem(QObject *item); + public slots: void onCardSelectionChanged(AbstractCardItem *card, bool selected); void onCardRightClicked(AbstractCardItem *card, QPoint screenPos); 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 634df0b15..fa6de81c2 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,8 +116,13 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::setTapAnimation); + connect(&enableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::enableAllAnimations); + connect(&disableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::disableAllAnimations); + auto *animationGrid = new QGridLayout; - animationGrid->addWidget(&tapAnimationCheckBox, 0, 0); + animationGrid->addWidget(&enableAllAnimationsButton, 0, 0); + animationGrid->addWidget(&disableAllAnimationsButton, 0, 1); + animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); animationGroupBox = new QGroupBox; animationGroupBox->setLayout(animationGrid); @@ -268,6 +273,16 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } +void UserInterfaceSettingsPage::enableAllAnimations() +{ + tapAnimationCheckBox.setChecked(true); +} + +void UserInterfaceSettingsPage::disableAllAnimations() +{ + tapAnimationCheckBox.setChecked(false); +} + void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() { const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); @@ -310,6 +325,8 @@ void UserInterfaceSettingsPage::retranslateUi() specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect")); animationGroupBox->setTitle(tr("Animation settings")); + enableAllAnimationsButton.setText(tr("&Enable all animations")); + disableAllAnimationsButton.setText(tr("&Disable all animations")); tapAnimationCheckBox.setText(tr("&Tap/untap animation")); deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default")); 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 9e6fada69..f98e723b8 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 @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,8 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); + void enableAllAnimations(); + void disableAllAnimations(); void updateCommanderSpellbookUiState(); private: @@ -34,6 +37,8 @@ private: QCheckBox showTotalSelectionCountCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; + QPushButton enableAllAnimationsButton; + QPushButton disableAllAnimationsButton; QCheckBox tapAnimationCheckBox; QCheckBox openDeckInNewTabCheckBox; QLabel visualDeckStoragePromptForConversionLabel;