Rework animation, global toggle for all animation checkboxes

Took 20 minutes
This commit is contained in:
Lukas Brübach 2026-08-11 18:09:59 +02:00
parent 15027497ee
commit 42bd25835a
15 changed files with 238 additions and 89 deletions

View file

@ -4,25 +4,43 @@
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../game_scene.h"
#include "../player/player_target.h" #include "../player/player_target.h"
#include "../z_values.h" #include "../z_values.h"
#include "../zones/card_zone.h" #include "../zones/card_zone.h"
#include "card_item.h" #include "card_item.h"
#include <QDebug> #include <QDebug>
#include <QElapsedTimer>
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QPainter> #include <QPainter>
#include <QTimer>
#include <QtMath> #include <QtMath>
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/command_attach_card.pb.h> #include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h> #include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h> #include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/color.h> #include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
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<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem)
: data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem)
{ {
@ -48,6 +66,13 @@ ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startI
} }
} }
ArrowItem::~ArrowItem()
{
if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
scene->unregisterAnimationItem(this);
}
}
void ArrowItem::onTargetDestroyed() void ArrowItem::onTargetDestroyed()
{ {
emit requestDeletion(data->creatorId, data->id); emit requestDeletion(data->creatorId, data->id);
@ -92,7 +117,10 @@ void ArrowItem::updatePath(const QPointF &endPoint)
prepareGeometryChange(); prepareGeometryChange();
if (lineLength < 30) { if (lineLength < 30) {
path = QPainterPath(); path = QPainterPath();
bodyPath = QPainterPath();
headPath = QPainterPath();
centerLine = QPainterPath(); centerLine = QPainterPath();
headBaseFraction = 1.0;
} else { } else {
QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength); QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength);
@ -100,9 +128,9 @@ void ArrowItem::updatePath(const QPointF &endPoint)
centerLine.moveTo(0, 0); centerLine.moveTo(0, 0);
centerLine.quadTo(c, QPointF(lineLength, 0)); centerLine.quadTo(c, QPointF(lineLength, 0));
double percentage = 1 - headLength / lineLength; headBaseFraction = 1 - headLength / lineLength;
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage); QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction);
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001)); QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001));
qreal alpha = testLine.angle() - 90; qreal alpha = testLine.angle() - 90;
QPointF endPoint1 = QPointF endPoint1 =
arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180)); arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180));
@ -113,14 +141,29 @@ void ArrowItem::updatePath(const QPointF &endPoint)
QPointF point2 = QPointF point2 =
endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180)); 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.quadTo(c, endPoint1);
path.lineTo(point1); path.lineTo(point1);
path.lineTo(QPointF(lineLength, 0)); path.lineTo(QPointF(lineLength, 0));
path.lineTo(point2); path.lineTo(point2);
path.lineTo(endPoint2); path.lineTo(endPoint2);
path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); path.quadTo(c, start2);
path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); 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);
} }
setPos(startPoint); setPos(startPoint);
@ -129,28 +172,40 @@ void ArrowItem::updatePath(const QPointF &endPoint)
void ArrowItem::startDrawAnimation() void ArrowItem::startDrawAnimation()
{ {
if (!SettingsCache::instance().userInterface().getAnimationsEnabled() || centerLine.isEmpty()) { if (!SettingsCache::instance().cardsDisplay().getArrowDrawAnimation() || centerLine.isEmpty()) {
return; return;
} }
strokeDurationMs = qBound(kMinStrokeDurationMs, centerLine.length() * kMsPerPixel, kMaxStrokeDurationMs);
glowFadeDurationMs = kGlowFadeDurationMs;
animationClock.start();
drawProgress = 0.0; drawProgress = 0.0;
if (drawTimer == nullptr) { glowAlpha = 1.0;
drawTimer = new QTimer(this); update();
connect(drawTimer, &QTimer::timeout, this, [this] { if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
if (!SettingsCache::instance().userInterface().getAnimationsEnabled()) { scene->registerAnimationItem(this);
drawProgress = 1.0;
drawTimer->stop();
} else {
drawProgress += 0.08;
if (drawProgress >= 1.0) {
drawProgress = 1.0;
drawTimer->stop();
}
}
update();
});
} }
drawTimer->start(20); }
bool ArrowItem::animationEvent()
{
const qint64 elapsed = animationClock.elapsed();
if (elapsed >= strokeDurationMs + glowFadeDurationMs) {
drawProgress = 1.0;
glowAlpha = 0.0;
update();
return false;
}
if (elapsed < strokeDurationMs) {
drawProgress = easeOutCubic(qBound<qreal>(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*/) void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
@ -163,25 +218,46 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
} }
painter->save(); painter->save();
if (drawProgress < 1.0 && !path.isEmpty()) {
QRectF pr = path.boundingRect();
qreal revealX = pr.left() + pr.width() * drawProgress;
QPainterPath clip;
clip.addRect(QRectF(pr.left() - 100, pr.top() - 100, revealX - pr.left() + 200, pr.height() + 200));
painter->setClipPath(clip);
}
painter->setBrush(paintColor); painter->setBrush(paintColor);
painter->drawPath(path);
if (drawProgress < 1.0 && !centerLine.isEmpty()) { if (drawProgress >= 1.0 || path.isEmpty()) {
const QPointF impulse = centerLine.pointAtPercent(drawProgress); painter->drawPath(path);
QRadialGradient glow(impulse, 10); } else if (drawProgress < headBaseFraction) {
glow.setColorAt(0, QColor(255, 255, 255, 230)); // The reveal edge and the sheen share the same arc-length parameterization,
glow.setColorAt(0.4, paintColor); // so the stroke stays exactly in sync with the trailing sheen.
glow.setColorAt(1, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0)); 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);
painter->drawPath(bodyPath);
} 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.
painter->drawPath(bodyPath);
const qreal headFadeIn = (drawProgress - headBaseFraction) / (1.0 - headBaseFraction);
painter->setOpacity(headFadeIn);
painter->drawPath(headPath);
painter->setOpacity(1.0);
}
if (glowAlpha > 0.0 && !centerLine.isEmpty()) {
// Sweep a bright band across the arrow instead of a fuzzy glow dot. Clipping to the
// silhouette keeps it flat against the shaft so it reads as a light reflection.
const qreal anticipation = qMin<qreal>(1.0, drawProgress / 0.08);
const QPointF sweep = centerLine.pointAtPercent(qMin<qreal>(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->setPen(Qt::NoPen);
painter->setBrush(glow); painter->setClipPath(path);
painter->drawEllipse(impulse, 8, 8); 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(); painter->restore();
} }

View file

@ -4,14 +4,15 @@
#include "../../game/board/arrow_data.h" #include "../../game/board/arrow_data.h"
#include "arrow_target.h" #include "arrow_target.h"
#include <QElapsedTimer>
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QPainterPath>
#include <QPointer> #include <QPointer>
#include <QSharedPointer> #include <QSharedPointer>
class CardItem; class CardItem;
class QGraphicsSceneMouseEvent; class QGraphicsSceneMouseEvent;
class PlayerLogic; class PlayerLogic;
class QTimer;
class ArrowItem : public QObject, public QGraphicsItem class ArrowItem : public QObject, public QGraphicsItem
{ {
@ -22,9 +23,17 @@ signals:
private: private:
QPainterPath path; QPainterPath path;
QPainterPath bodyPath;
QPainterPath headPath;
QPainterPath centerLine; QPainterPath centerLine;
qreal headBaseFraction = 1.0;
QElapsedTimer animationClock;
qreal strokeDurationMs = 0;
qreal glowFadeDurationMs = 0;
qreal drawProgress = 1.0; qreal drawProgress = 1.0;
QTimer *drawTimer = nullptr; qreal glowAlpha = 0.0;
static constexpr qreal glowExtent = 12.0;
protected: protected:
QSharedPointer<const ArrowData> data; QSharedPointer<const ArrowData> data;
@ -37,17 +46,19 @@ protected:
public: public:
ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
~ArrowItem() override;
void onTargetDestroyed(); void onTargetDestroyed();
void delArrow(); void delArrow();
void updatePath(); void updatePath();
void updatePath(const QPointF &endPoint); void updatePath(const QPointF &endPoint);
void startDrawAnimation(); void startDrawAnimation();
bool animationEvent();
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] QRectF boundingRect() const override [[nodiscard]] QRectF boundingRect() const override
{ {
return path.boundingRect(); return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent);
} }
[[nodiscard]] QPainterPath shape() const override [[nodiscard]] QPainterPath shape() const override
{ {
@ -111,4 +122,4 @@ protected:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
}; };
#endif #endif

View file

@ -45,18 +45,19 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
GameScene::~GameScene() GameScene::~GameScene()
{ {
delete animationTimer;
// Delete all ArrowItems before QGraphicsScene's base destructor runs. // Delete all ArrowItems before QGraphicsScene's base destructor runs.
// QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order. // QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order.
// If a PlayerTarget is destroyed before an ArrowItem pointing to it, // If a PlayerTarget is destroyed before an ArrowItem pointing to it,
// ArrowItem::onTargetDestroyed fires and emits on the partially-destroyed // ArrowItem::onTargetDestroyed fires and emits on the partially-destroyed
// GameScene, causing a segfault. // GameScene, causing a segfault.
// The arrows must also be deleted before animationTimer so their
// destructors can unregister from the still-valid timer.
for (auto *item : items()) { for (auto *item : items()) {
if (auto *arrow = qgraphicsitem_cast<ArrowItem *>(item)) { if (auto *arrow = qgraphicsitem_cast<ArrowItem *>(item)) {
delete arrow; delete arrow;
} }
} }
delete animationTimer;
// DO NOT call clearViews() here // DO NOT call clearViews() here
// clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults // clearViews calls close() on the zoneViews, which sends signals; sending signals in destructors leads to segfaults
@ -495,8 +496,8 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
} }
auto *arrow = new ArrowItem(data, startCard, targetItem); auto *arrow = new ArrowItem(data, startCard, targetItem);
arrow->startDrawAnimation();
addItem(arrow); addItem(arrow);
arrow->startDrawAnimation();
arrowRegistry.insert(data, arrow); arrowRegistry.insert(data, arrow);
connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion);
} }
@ -744,7 +745,14 @@ void GameScene::timerEvent(QTimerEvent * /*event*/)
i.remove(); i.remove();
} }
} }
if (cardsToAnimate.isEmpty()) { QMutableSetIterator<ArrowItem *> j(arrowsToAnimate);
while (j.hasNext()) {
j.next();
if (!j.value()->animationEvent()) {
j.remove();
}
}
if (cardsToAnimate.isEmpty() && arrowsToAnimate.isEmpty()) {
animationTimer->stop(); animationTimer->stop();
} }
} }
@ -760,7 +768,23 @@ void GameScene::registerAnimationItem(AbstractCardItem *card)
void GameScene::unregisterAnimationItem(AbstractCardItem *card) void GameScene::unregisterAnimationItem(AbstractCardItem *card)
{ {
cardsToAnimate.remove(static_cast<CardItem *>(card)); cardsToAnimate.remove(static_cast<CardItem *>(card));
if (cardsToAnimate.isEmpty()) { if (cardsToAnimate.isEmpty() && arrowsToAnimate.isEmpty()) {
animationTimer->stop();
}
}
void GameScene::registerAnimationItem(ArrowItem *arrow)
{
arrowsToAnimate.insert(arrow);
if (!animationTimer->isActive()) {
animationTimer->start(10, this);
}
}
void GameScene::unregisterAnimationItem(ArrowItem *arrow)
{
arrowsToAnimate.remove(arrow);
if (cardsToAnimate.isEmpty() && arrowsToAnimate.isEmpty()) {
animationTimer->stop(); animationTimer->stop();
} }
} }

View file

@ -52,6 +52,7 @@ private:
QPointer<CardItem> hoveredCard; ///< Currently hovered card QPointer<CardItem> hoveredCard; ///< Currently hovered card
QBasicTimer *animationTimer; ///< Timer for card animations QBasicTimer *animationTimer; ///< Timer for card animations
QSet<CardItem *> cardsToAnimate; ///< Cards currently animating QSet<CardItem *> cardsToAnimate; ///< Cards currently animating
QSet<ArrowItem *> arrowsToAnimate; ///< Arrows currently animating
int playerRotation; ///< Rotation offset for player layout int playerRotation; ///< Rotation offset for player layout
/** /**
@ -187,6 +188,12 @@ public:
/** @brief Unregisters a card from animation updates. */ /** @brief Unregisters a card from animation updates. */
void unregisterAnimationItem(AbstractCardItem *card); void unregisterAnimationItem(AbstractCardItem *card);
/** @brief Registers an arrow for animation updates. */
void registerAnimationItem(ArrowItem *arrow);
/** @brief Unregisters an arrow from animation updates. */
void unregisterAnimationItem(ArrowItem *arrow);
void startRubberBand(const QPointF &selectionOrigin); void startRubberBand(const QPointF &selectionOrigin);
void resizeRubberBand(const QPointF &cursorPoint, int selectedCount); void resizeRubberBand(const QPointF &cursorPoint, int selectedCount);
void stopRubberBand(); void stopRubberBand();

View file

@ -116,13 +116,25 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setTapAnimation); &CardsDisplaySettings::setTapAnimation);
animationsEnabledCheckBox.setChecked(SettingsCache::instance().userInterface().getAnimationsEnabled()); arrowDrawAnimationCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getArrowDrawAnimation());
connect(&animationsEnabledCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), connect(&arrowDrawAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
&InterfaceSettings::setAnimationsEnabled); &CardsDisplaySettings::setArrowDrawAnimation);
visualDeckStorageSelectionAnimationCheckBox.setChecked(
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSelectionAnimation());
connect(&visualDeckStorageSelectionAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED,
&SettingsCache::instance().visualDeckStorage(),
&VisualDeckStorageSettings::setVisualDeckStorageSelectionAnimation);
connect(&enableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::enableAllAnimations);
connect(&disableAllAnimationsButton, &QPushButton::clicked, this, &UserInterfaceSettingsPage::disableAllAnimations);
auto *animationGrid = new QGridLayout; auto *animationGrid = new QGridLayout;
animationGrid->addWidget(&animationsEnabledCheckBox, 0, 0); animationGrid->addWidget(&enableAllAnimationsButton, 0, 0);
animationGrid->addWidget(&disableAllAnimationsButton, 0, 1);
animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); animationGrid->addWidget(&tapAnimationCheckBox, 1, 0);
animationGrid->addWidget(&arrowDrawAnimationCheckBox, 2, 0);
animationGrid->addWidget(&visualDeckStorageSelectionAnimationCheckBox, 3, 0);
animationGroupBox = new QGroupBox; animationGroupBox = new QGroupBox;
animationGroupBox->setLayout(animationGrid); animationGroupBox->setLayout(animationGrid);
@ -137,12 +149,6 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&visualDeckStorageInGameCheckBox, &QCheckBox::QT_STATE_CHANGED, connect(&visualDeckStorageInGameCheckBox, &QCheckBox::QT_STATE_CHANGED,
&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setVisualDeckStorageInGame); &SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::setVisualDeckStorageInGame);
visualDeckStorageSelectionAnimationCheckBox.setChecked(
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSelectionAnimation());
connect(&visualDeckStorageSelectionAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED,
&SettingsCache::instance().visualDeckStorage(),
&VisualDeckStorageSettings::setVisualDeckStorageSelectionAnimation);
visualDeckStoragePromptForConversionSelector.addItem(""); // these will be set in retranslateUI visualDeckStoragePromptForConversionSelector.addItem(""); // these will be set in retranslateUI
visualDeckStoragePromptForConversionSelector.addItem(""); visualDeckStoragePromptForConversionSelector.addItem("");
visualDeckStoragePromptForConversionSelector.addItem(""); visualDeckStoragePromptForConversionSelector.addItem("");
@ -221,15 +227,14 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
auto *deckEditorGrid = new QGridLayout; auto *deckEditorGrid = new QGridLayout;
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0); deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0);
deckEditorGrid->addWidget(&visualDeckStorageSelectionAnimationCheckBox, 2, 0); deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionLabel, 2, 0);
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionLabel, 3, 0); deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 2, 1);
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 3, 0);
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 3, 1);
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 4, 0);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0); deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 4, 1);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1); deckEditorGrid->addWidget(labelWidget, 5, 0);
deckEditorGrid->addWidget(labelWidget, 6, 0); deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 5, 1);
deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1);
deckEditorGroupBox = new QGroupBox; deckEditorGroupBox = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid); deckEditorGroupBox->setLayout(deckEditorGrid);
@ -273,6 +278,20 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i)
} }
} }
void UserInterfaceSettingsPage::enableAllAnimations()
{
tapAnimationCheckBox.setChecked(true);
arrowDrawAnimationCheckBox.setChecked(true);
visualDeckStorageSelectionAnimationCheckBox.setChecked(true);
}
void UserInterfaceSettingsPage::disableAllAnimations()
{
tapAnimationCheckBox.setChecked(false);
arrowDrawAnimationCheckBox.setChecked(false);
visualDeckStorageSelectionAnimationCheckBox.setChecked(false);
}
void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() void UserInterfaceSettingsPage::updateCommanderSpellbookUiState()
{ {
const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled();
@ -315,12 +334,14 @@ void UserInterfaceSettingsPage::retranslateUi()
specNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar for game events while you are spectating")); 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")); buddyConnectNotificationsEnabledCheckBox.setText(tr("Notify in the taskbar when users in your buddy list connect"));
animationGroupBox->setTitle(tr("Animation settings")); animationGroupBox->setTitle(tr("Animation settings"));
animationsEnabledCheckBox.setText(tr("Enable game &animations")); enableAllAnimationsButton.setText(tr("&Enable all animations"));
disableAllAnimationsButton.setText(tr("&Disable all animations"));
tapAnimationCheckBox.setText(tr("&Tap/untap animation")); tapAnimationCheckBox.setText(tr("&Tap/untap animation"));
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
visualDeckStorageSelectionAnimationCheckBox.setText(tr("Use selection animation for Visual Deck Storage"));
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default")); openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby")); visualDeckStorageInGameCheckBox.setText(tr("Use visual deck storage in game lobby"));
visualDeckStorageSelectionAnimationCheckBox.setText(tr("Use selection animation for Visual Deck Storage"));
visualDeckStoragePromptForConversionLabel.setText( visualDeckStoragePromptForConversionLabel.setText(
tr("When adding a tag in the visual deck storage to a .txt deck:")); tr("When adding a tag in the visual deck storage to a .txt deck:"));
visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexNone, visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexNone,

View file

@ -7,6 +7,7 @@
#include <QComboBox> #include <QComboBox>
#include <QGroupBox> #include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QPushButton>
#include <QSpinBox> #include <QSpinBox>
#include <QToolButton> #include <QToolButton>
#include <libcockatrice/settings/cards_display_settings.h> #include <libcockatrice/settings/cards_display_settings.h>
@ -17,6 +18,8 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage
Q_OBJECT Q_OBJECT
private slots: private slots:
void setNotificationEnabled(QT_STATE_CHANGED_T); void setNotificationEnabled(QT_STATE_CHANGED_T);
void enableAllAnimations();
void disableAllAnimations();
void updateCommanderSpellbookUiState(); void updateCommanderSpellbookUiState();
private: private:
@ -35,7 +38,9 @@ private:
QCheckBox useTearOffMenusCheckBox; QCheckBox useTearOffMenusCheckBox;
QCheckBox keepGameChatFocusCheckBox; QCheckBox keepGameChatFocusCheckBox;
QCheckBox tapAnimationCheckBox; QCheckBox tapAnimationCheckBox;
QCheckBox animationsEnabledCheckBox; QCheckBox arrowDrawAnimationCheckBox;
QPushButton enableAllAnimationsButton;
QPushButton disableAllAnimationsButton;
QCheckBox openDeckInNewTabCheckBox; QCheckBox openDeckInNewTabCheckBox;
QLabel visualDeckStoragePromptForConversionLabel; QLabel visualDeckStoragePromptForConversionLabel;
QComboBox visualDeckStoragePromptForConversionSelector; QComboBox visualDeckStoragePromptForConversionSelector;

View file

@ -212,7 +212,8 @@ void VisualDeckStorageWidget::updateTagsVisibility(const bool visible)
} }
} }
void VisualDeckStorageWidget::updateSelectionAnimationEnabled(const bool enabled) void VisualDeckStorageWidget::updateSelectionAnimationEnabled()
{ {
deckPreviewSelectionAnimationEnabled = enabled; deckPreviewSelectionAnimationEnabled =
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageSelectionAnimation();
} }

View file

@ -45,7 +45,7 @@ public slots:
void updateColorFilter(); void updateColorFilter();
void updateSearchFilter(); void updateSearchFilter();
void updateTagsVisibility(bool visible); void updateTagsVisibility(bool visible);
void updateSelectionAnimationEnabled(bool enabled); void updateSelectionAnimationEnabled();
void updateSortOrder(); void updateSortOrder();
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override; void showEvent(QShowEvent *event) override;

View file

@ -15,6 +15,7 @@ public:
[[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0;
[[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0;
[[nodiscard]] virtual bool getTapAnimation() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0;
[[nodiscard]] virtual bool getArrowDrawAnimation() const = 0;
[[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0;
[[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0;
[[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0;

View file

@ -39,7 +39,6 @@ public:
[[nodiscard]] virtual bool getShowStatusBar() const = 0; [[nodiscard]] virtual bool getShowStatusBar() const = 0;
[[nodiscard]] virtual bool getShowShortcuts() const = 0; [[nodiscard]] virtual bool getShowShortcuts() const = 0;
[[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0; [[nodiscard]] virtual bool getShowGameSelectorFilterToolbar() const = 0;
[[nodiscard]] virtual bool getAnimationsEnabled() const = 0;
}; };
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H #endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H

View file

@ -50,6 +50,11 @@ bool CardsDisplaySettings::getTapAnimation() const
return getValue("tapAnimation", QString(), QString(), true).toBool(); return getValue("tapAnimation", QString(), QString(), true).toBool();
} }
bool CardsDisplaySettings::getArrowDrawAnimation() const
{
return getValue("arrowDrawAnimation", QString(), QString(), true).toBool();
}
bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const
{ {
return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool(); return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool();
@ -159,6 +164,11 @@ void CardsDisplaySettings::setTapAnimation(bool _tapAnimation)
setValue(_tapAnimation, "tapAnimation"); setValue(_tapAnimation, "tapAnimation");
} }
void CardsDisplaySettings::setArrowDrawAnimation(bool _arrowDrawAnimation)
{
setValue(_arrowDrawAnimation, "arrowDrawAnimation");
}
void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards) void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards)
{ {
setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards"); setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards");

View file

@ -20,6 +20,7 @@ public:
[[nodiscard]] bool getIncludeRebalancedCards() const override; [[nodiscard]] bool getIncludeRebalancedCards() const override;
[[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override; [[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override;
[[nodiscard]] bool getTapAnimation() const override; [[nodiscard]] bool getTapAnimation() const override;
[[nodiscard]] bool getArrowDrawAnimation() const override;
[[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override; [[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override;
[[nodiscard]] bool getScaleCards() const override; [[nodiscard]] bool getScaleCards() const override;
[[nodiscard]] int getStackCardOverlapPercent() const override; [[nodiscard]] int getStackCardOverlapPercent() const override;
@ -40,6 +41,7 @@ public:
void setIncludeRebalancedCards(bool _includeRebalancedCards); void setIncludeRebalancedCards(bool _includeRebalancedCards);
void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible); void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible);
void setTapAnimation(bool _tapAnimation); void setTapAnimation(bool _tapAnimation);
void setArrowDrawAnimation(bool _arrowDrawAnimation);
void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards); void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards);
void setCardScaling(bool _scaleCards); void setCardScaling(bool _scaleCards);
void setStackCardOverlapPercent(int _verticalCardOverlapPercent); void setStackCardOverlapPercent(int _verticalCardOverlapPercent);

View file

@ -160,11 +160,6 @@ bool InterfaceSettings::getShowGameSelectorFilterToolbar() const
return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool(); return getValue("showGameSelectorFilterToolbar", QString(), QString(), true).toBool();
} }
bool InterfaceSettings::getAnimationsEnabled() const
{
return getValue("animationsEnabled", QString(), QString(), true).toBool();
}
void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus) void InterfaceSettings::setUseTearOffMenus(bool _useTearOffMenus)
{ {
setValue(_useTearOffMenus, "useTearOffMenus"); setValue(_useTearOffMenus, "useTearOffMenus");
@ -331,9 +326,3 @@ void InterfaceSettings::setShowGameSelectorFilterToolbar(bool _showGameSelectorF
setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar"); setValue(_showGameSelectorFilterToolbar, "showGameSelectorFilterToolbar");
emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar); emit showGameSelectorFilterToolbarChanged(_showGameSelectorFilterToolbar);
} }
void InterfaceSettings::setAnimationsEnabled(bool _animationsEnabled)
{
setValue(_animationsEnabled, "animationsEnabled");
emit animationsEnabledChanged(_animationsEnabled);
}

View file

@ -42,7 +42,6 @@ public:
[[nodiscard]] bool getShowStatusBar() const override; [[nodiscard]] bool getShowStatusBar() const override;
[[nodiscard]] bool getShowShortcuts() const override; [[nodiscard]] bool getShowShortcuts() const override;
[[nodiscard]] bool getShowGameSelectorFilterToolbar() const override; [[nodiscard]] bool getShowGameSelectorFilterToolbar() const override;
[[nodiscard]] bool getAnimationsEnabled() const override;
void setUseTearOffMenus(bool _useTearOffMenus); void setUseTearOffMenus(bool _useTearOffMenus);
void setCardViewInitialRowsMax(int _cardViewInitialRowsMax); void setCardViewInitialRowsMax(int _cardViewInitialRowsMax);
@ -75,7 +74,6 @@ public:
void setShowStatusBar(bool _showStatusBar); void setShowStatusBar(bool _showStatusBar);
void setShowShortcuts(bool _showShortcuts); void setShowShortcuts(bool _showShortcuts);
void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar); void setShowGameSelectorFilterToolbar(bool _showGameSelectorFilterToolbar);
void setAnimationsEnabled(bool _animationsEnabled);
signals: signals:
void useTearOffMenusChanged(bool state); void useTearOffMenusChanged(bool state);
@ -87,7 +85,6 @@ signals:
void tallyTypeChanged(int type); void tallyTypeChanged(int type);
void showStatusBarChanged(bool state); void showStatusBarChanged(bool state);
void showGameSelectorFilterToolbarChanged(bool state); void showGameSelectorFilterToolbarChanged(bool state);
void animationsEnabledChanged(bool state);
public: public:
explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr); explicit InterfaceSettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -476,6 +476,12 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default)
ASSERT_EQ(s.getSampleHandSize(), 7); ASSERT_EQ(s.getSampleHandSize(), 7);
} }
TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getArrowDrawAnimation(), true);
}
// --- VisualDeckStorageSettings --- // --- VisualDeckStorageSettings ---
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)