[Settings] Add hover zoom/panel triggers, delays, and double-confirm

This commit is contained in:
Lukas Brübach 2026-10-14 19:20:32 +02:00 • committed by Lukas Brübach
parent c7e8422808
commit 92f05fd363
7 changed files with 282 additions and 0 deletions

View file

@ -569,6 +569,9 @@ private:
{"Player/aRevealToAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Reveal Selected Cards to All Players"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},
{"Player/aShowHoverPanel", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Show Hover Panel for Card"),
parseSequenceString(""),
ShortcutGroup::Playing_Area)},
{"Player/aMoveToBottomLibrary", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Bottom of Library"),
parseSequenceString("Ctrl+B"),
ShortcutGroup::Move_selected)},

View file

@ -28,6 +28,14 @@ enum visualDeckStoragePromptForConversionIndex
visualDeckStoragePromptForConversionIndexAlways
};
// A corrupt or out-of-range stored trigger would otherwise select index -1 and leave
// the row half-hidden; clamp it back into the known trigger range.
static int clampHoverTrigger(int trigger)
{
return qBound(static_cast<int>(CardsDisplaySettings::HoverPanelTriggerHover), trigger,
static_cast<int>(CardsDisplaySettings::HoverPanelTriggerShortcut));
}
UserInterfaceSettingsPage::UserInterfaceSettingsPage()
{
// general settings
@ -154,6 +162,66 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
animationGroupBox = new QGroupBox;
animationGroupBox->setLayout(animationGrid);
// hover panel settings
hoverZoomTriggerSelector.addItem(QString());
hoverZoomTriggerSelector.addItem(QString());
hoverZoomTriggerSelector.addItem(QString());
hoverZoomTriggerSelector.addItem(QString());
hoverZoomTriggerSelector.setCurrentIndex(
clampHoverTrigger(SettingsCache::instance().cardsDisplay().getHoverZoomTrigger()));
connect(&hoverZoomTriggerSelector, &QComboBox::currentIndexChanged, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setHoverZoomTrigger);
hoverPanelTriggerSelector.addItem(QString());
hoverPanelTriggerSelector.addItem(QString());
hoverPanelTriggerSelector.addItem(QString());
hoverPanelTriggerSelector.addItem(QString());
hoverPanelTriggerSelector.setCurrentIndex(
clampHoverTrigger(SettingsCache::instance().cardsDisplay().getHoverPanelTrigger()));
connect(&hoverPanelTriggerSelector, &QComboBox::currentIndexChanged, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setHoverPanelTrigger);
hoverPanelDoubleConfirmCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getHoverPanelDoubleConfirm());
connect(&hoverPanelDoubleConfirmCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setHoverPanelDoubleConfirm);
hoverZoomDelayBox.setRange(0, 5000);
hoverZoomDelayBox.setSingleStep(50);
hoverZoomDelayBox.setSuffix(" ms");
hoverZoomDelayBox.setValue(SettingsCache::instance().cardsDisplay().getHoverZoomDelayMs());
connect(&hoverZoomDelayBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setHoverZoomDelayMs);
hoverPanelDelayBox.setRange(0, 5000);
hoverPanelDelayBox.setSingleStep(50);
hoverPanelDelayBox.setSuffix(" ms");
hoverPanelDelayBox.setValue(SettingsCache::instance().cardsDisplay().getHoverPanelDelayMs());
connect(&hoverPanelDelayBox, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setHoverPanelDelayMs);
auto *hoverPanelGrid = new QGridLayout;
hoverPanelGrid->addWidget(&hoverZoomTriggerLabel, 0, 0);
hoverPanelGrid->addWidget(&hoverZoomTriggerSelector, 0, 1);
hoverPanelGrid->addWidget(&hoverPanelTriggerLabel, 1, 0);
hoverPanelGrid->addWidget(&hoverPanelTriggerSelector, 1, 1);
hoverPanelGrid->addWidget(&hoverPanelDoubleConfirmCheckBox, 2, 0, 1, 2);
hoverPanelGrid->addWidget(&hoverZoomDelayLabel, 3, 0);
hoverPanelGrid->addWidget(&hoverZoomDelayBox, 3, 1);
hoverPanelGrid->addWidget(&hoverPanelDelayLabel, 4, 0);
hoverPanelGrid->addWidget(&hoverPanelDelayBox, 4, 1);
hoverWheelNoteLabel.setWordWrap(true);
hoverPanelGrid->addWidget(&hoverWheelNoteLabel, 5, 0, 1, 2);
connect(&hoverZoomTriggerSelector, &QComboBox::currentIndexChanged, this,
&UserInterfaceSettingsPage::updateHoverPanelUiState);
connect(&hoverPanelTriggerSelector, &QComboBox::currentIndexChanged, this,
&UserInterfaceSettingsPage::updateHoverPanelUiState);
connect(&hoverPanelDoubleConfirmCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
&UserInterfaceSettingsPage::updateHoverPanelUiState);
updateHoverPanelUiState();
hoverPanelGroupBox = new QGroupBox;
hoverPanelGroupBox->setLayout(hoverPanelGrid);
// deck editor settings
openDeckInNewTabCheckBox.setChecked(SettingsCache::instance().deckEditor().getOpenDeckInNewTab());
connect(&openDeckInNewTabCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().deckEditor(),
@ -293,6 +361,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
mainLayout->addWidget(generalGroupBox);
mainLayout->addWidget(notificationsGroupBox);
mainLayout->addWidget(animationGroupBox);
mainLayout->addWidget(hoverPanelGroupBox);
mainLayout->addWidget(deckEditorGroupBox);
mainLayout->addWidget(replayGroupBox);
mainLayout->addStretch();
@ -351,6 +420,29 @@ void UserInterfaceSettingsPage::updateCommanderSpellbookUiState()
}
}
void UserInterfaceSettingsPage::updateHoverPanelUiState()
{
// Phase timers only apply to the hover trigger; double-confirm only applies when
// both phases share one trigger (repeating it).
const auto hoverTrigger = CardsDisplaySettings::HoverPanelTriggerHover;
const auto wheelTrigger = CardsDisplaySettings::HoverPanelTriggerWheel;
const bool zoomOnHover = hoverZoomTriggerSelector.currentIndex() == hoverTrigger;
const bool panelOnHover = hoverPanelTriggerSelector.currentIndex() == hoverTrigger;
const bool sharedTrigger = hoverZoomTriggerSelector.currentIndex() == hoverPanelTriggerSelector.currentIndex();
hoverZoomDelayLabel.setVisible(zoomOnHover);
hoverZoomDelayBox.setVisible(zoomOnHover);
hoverPanelDelayLabel.setVisible(panelOnHover);
hoverPanelDelayBox.setVisible(panelOnHover);
hoverPanelDoubleConfirmCheckBox.setEnabled(sharedTrigger);
const bool panelAuto = panelOnHover && !(sharedTrigger && hoverPanelDoubleConfirmCheckBox.isChecked());
hoverPanelDelayLabel.setEnabled(panelAuto);
hoverPanelDelayBox.setEnabled(panelAuto);
// The wheel trigger consumes wheel events over cards, so the board cannot scroll there.
const bool wheelBound = hoverZoomTriggerSelector.currentIndex() == wheelTrigger ||
hoverPanelTriggerSelector.currentIndex() == wheelTrigger;
hoverWheelNoteLabel.setVisible(wheelBound);
}
void UserInterfaceSettingsPage::retranslateUi()
{
generalGroupBox->setTitle(tr("General interface settings"));
@ -380,6 +472,21 @@ void UserInterfaceSettingsPage::retranslateUi()
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
hoverPanelGroupBox->setTitle(tr("Hover panel settings"));
hoverPanelTriggerLabel.setText(tr("Show the card panel on:"));
hoverZoomTriggerLabel.setText(tr("Zoom the card on:"));
for (auto *selector : {&hoverPanelTriggerSelector, &hoverZoomTriggerSelector}) {
selector->setItemText(CardsDisplaySettings::HoverPanelTriggerHover, tr("Hover"));
selector->setItemText(CardsDisplaySettings::HoverPanelTriggerRightClick, tr("Right-click"));
selector->setItemText(CardsDisplaySettings::HoverPanelTriggerWheel, tr("Mouse wheel"));
selector->setItemText(CardsDisplaySettings::HoverPanelTriggerShortcut, tr("Keyboard shortcut"));
}
hoverPanelDoubleConfirmCheckBox.setText(tr("Zoom first; repeat the action to show the panel"));
hoverZoomDelayLabel.setText(tr("Time before the zoom kicks in:"));
hoverPanelDelayLabel.setText(tr("Time before the tab panel shows up:"));
hoverWheelNoteLabel.setText(
tr("Note: the mouse wheel action replaces board scrolling over cards; scroll up to zoom back out."));
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));
openDeckInNewTabCheckBox.setText(tr("Open deck in new tab by default"));
visualDeckEditorShowCardCountsCheckBox.setText(tr("Show card counts in Visual Deck Editor"));

View file

@ -22,6 +22,7 @@ private slots:
void enableAllAnimations();
void disableAllAnimations();
void updateCommanderSpellbookUiState();
void updateHoverPanelUiState();
private:
QCheckBox doubleClickToPlayCheckBox;
@ -47,6 +48,17 @@ private:
QCheckBox lifeCounterAnimationsCheckBox;
QCheckBox battlefieldFlashCheckBox;
QLabel hoverPanelTriggerLabel;
QComboBox hoverPanelTriggerSelector;
QLabel hoverZoomTriggerLabel;
QComboBox hoverZoomTriggerSelector;
QCheckBox hoverPanelDoubleConfirmCheckBox;
QLabel hoverZoomDelayLabel;
QSpinBox hoverZoomDelayBox;
QLabel hoverPanelDelayLabel;
QSpinBox hoverPanelDelayBox;
QLabel hoverWheelNoteLabel;
QCheckBox openDeckInNewTabCheckBox;
QLabel visualDeckStoragePromptForConversionLabel;
QComboBox visualDeckStoragePromptForConversionSelector;
@ -69,6 +81,7 @@ private:
QGroupBox *generalGroupBox;
QGroupBox *notificationsGroupBox;
QGroupBox *animationGroupBox;
QGroupBox *hoverPanelGroupBox;
QGroupBox *deckEditorGroupBox;
QGroupBox *replayGroupBox;

View file

@ -19,6 +19,11 @@ public:
[[nodiscard]] virtual bool getArrowDrawAnimation() const = 0;
[[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0;
[[nodiscard]] virtual bool getScaleCards() const = 0;
[[nodiscard]] virtual int getHoverPanelTrigger() const = 0;
[[nodiscard]] virtual int getHoverZoomTrigger() const = 0;
[[nodiscard]] virtual bool getHoverPanelDoubleConfirm() const = 0;
[[nodiscard]] virtual int getHoverZoomDelayMs() const = 0;
[[nodiscard]] virtual int getHoverPanelDelayMs() const = 0;
[[nodiscard]] virtual int getStackCardOverlapPercent() const = 0;
[[nodiscard]] virtual int getCardInfoViewMode() const = 0;
[[nodiscard]] virtual int getVisualDeckStorageCardSize() const = 0;

View file

@ -68,6 +68,31 @@ bool CardsDisplaySettings::getScaleCards() const
return getValue("scaleCards", QString(), QString(), true).toBool();
}
int CardsDisplaySettings::getHoverPanelTrigger() const
{
return getValue("hoverPanelTrigger", QString(), QString(), 0).toInt();
}
int CardsDisplaySettings::getHoverZoomTrigger() const
{
return getValue("hoverZoomTrigger", QString(), QString(), 0).toInt();
}
bool CardsDisplaySettings::getHoverPanelDoubleConfirm() const
{
return getValue("hoverPanelDoubleConfirm", QString(), QString(), false).toBool();
}
int CardsDisplaySettings::getHoverZoomDelayMs() const
{
return getValue("hoverZoomDelayMs", QString(), QString(), 400).toInt();
}
int CardsDisplaySettings::getHoverPanelDelayMs() const
{
return getValue("hoverPanelDelayMs", QString(), QString(), 200).toInt();
}
int CardsDisplaySettings::getStackCardOverlapPercent() const
{
return getValue("verticalCardOverlapPercent", QString(), QString(), 33).toInt();
@ -188,6 +213,36 @@ void CardsDisplaySettings::setCardScaling(bool _scaleCards)
setValue(_scaleCards, "scaleCards");
}
void CardsDisplaySettings::setHoverPanelTrigger(int _trigger)
{
setValue(_trigger, "hoverPanelTrigger");
emit hoverPanelTriggerChanged(_trigger);
}
void CardsDisplaySettings::setHoverZoomTrigger(int _trigger)
{
setValue(_trigger, "hoverZoomTrigger");
emit hoverZoomTriggerChanged(_trigger);
}
void CardsDisplaySettings::setHoverPanelDoubleConfirm(bool _doubleConfirm)
{
setValue(_doubleConfirm, "hoverPanelDoubleConfirm");
emit hoverPanelDoubleConfirmChanged(_doubleConfirm);
}
void CardsDisplaySettings::setHoverZoomDelayMs(int _ms)
{
setValue(_ms, "hoverZoomDelayMs");
emit hoverZoomDelayMsChanged(_ms);
}
void CardsDisplaySettings::setHoverPanelDelayMs(int _ms)
{
setValue(_ms, "hoverPanelDelayMs");
emit hoverPanelDelayMsChanged(_ms);
}
void CardsDisplaySettings::setStackCardOverlapPercent(int _verticalCardOverlapPercent)
{
setValue(_verticalCardOverlapPercent, "verticalCardOverlapPercent");

View file

@ -15,6 +15,16 @@ class CardsDisplaySettings : public SettingsManager, public ICardsDisplaySetting
friend class SettingsCache;
public:
/// What can summon the hover zoom or panel. Stored as int; values are the
/// serialization contract, so never reorder or renumber.
enum HoverPanelTrigger
{
HoverPanelTriggerHover = 0,
HoverPanelTriggerRightClick = 1,
HoverPanelTriggerWheel = 2,
HoverPanelTriggerShortcut = 3
};
[[nodiscard]] bool getDisplayCardNames() const override;
[[nodiscard]] bool getRoundCardCorners() const override;
[[nodiscard]] bool getOverrideAllCardArtWithPersonalPreference() const override;
@ -26,6 +36,16 @@ public:
[[nodiscard]] bool getArrowDrawAnimation() const override;
[[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override;
[[nodiscard]] bool getScaleCards() const override;
/// What summons the in-game card hover panel (a HoverPanelTrigger value).
[[nodiscard]] int getHoverPanelTrigger() const override;
/// What zooms the hovered card (a HoverPanelTrigger value). Zoom and panel triggers are independent.
[[nodiscard]] int getHoverZoomTrigger() const override;
/// When true, the trigger first only zooms; repeating it shows the panel.
[[nodiscard]] bool getHoverPanelDoubleConfirm() const override;
/// Hover dwell before the zoom kicks in (ms). Only used with the hover trigger.
[[nodiscard]] int getHoverZoomDelayMs() const override;
/// Wait after the zoom before the tab panel shows (ms). Hover trigger, one-step only.
[[nodiscard]] int getHoverPanelDelayMs() const override;
[[nodiscard]] int getStackCardOverlapPercent() const override;
[[nodiscard]] int getCardInfoViewMode() const override;
[[nodiscard]] int getVisualDeckStorageCardSize() const override;
@ -49,6 +69,11 @@ public:
void setArrowDrawAnimation(bool _arrowDrawAnimation);
void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards);
void setCardScaling(bool _scaleCards);
void setHoverPanelTrigger(int _trigger);
void setHoverZoomTrigger(int _trigger);
void setHoverPanelDoubleConfirm(bool _doubleConfirm);
void setHoverZoomDelayMs(int _ms);
void setHoverPanelDelayMs(int _ms);
void setStackCardOverlapPercent(int _verticalCardOverlapPercent);
void setCardInfoViewMode(int _viewMode);
void setVisualDeckStorageCardSize(int _cardSize);
@ -78,6 +103,11 @@ signals:
void sampleHandSizeChanged(int amount);
void cardLangChanged(const QString &lang);
void cardSearchLanguageChanged(int cardSearchLanguage);
void hoverPanelTriggerChanged(int trigger);
void hoverZoomTriggerChanged(int trigger);
void hoverPanelDoubleConfirmChanged(bool doubleConfirm);
void hoverZoomDelayMsChanged(int ms);
void hoverPanelDelayMsChanged(int ms);
public:
explicit CardsDisplaySettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -683,6 +683,75 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
ASSERT_EQ(s.getArrowDrawAnimation(), true);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelTrigger_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHoverPanelTrigger(), 0);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelTrigger_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setHoverPanelTrigger(2);
ASSERT_EQ(s.getHoverPanelTrigger(), 2);
s.setHoverPanelTrigger(0);
ASSERT_EQ(s.getHoverPanelTrigger(), 0);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomTrigger_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHoverZoomTrigger(), 0);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomTrigger_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setHoverZoomTrigger(1);
ASSERT_EQ(s.getHoverZoomTrigger(), 1);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelDoubleConfirm_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHoverPanelDoubleConfirm(), false);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelDoubleConfirm_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setHoverPanelDoubleConfirm(true);
ASSERT_EQ(s.getHoverPanelDoubleConfirm(), true);
s.setHoverPanelDoubleConfirm(false);
ASSERT_EQ(s.getHoverPanelDoubleConfirm(), false);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomDelayMs_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHoverZoomDelayMs(), 400);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomDelayMs_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setHoverZoomDelayMs(750);
ASSERT_EQ(s.getHoverZoomDelayMs(), 750);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelDelayMs_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getHoverPanelDelayMs(), 200);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelDelayMs_SetAndGet)
{
CardsDisplaySettings s(settingsPath, nullptr);
s.setHoverPanelDelayMs(0);
ASSERT_EQ(s.getHoverPanelDelayMs(), 0);
}
TEST_F(SettingsDefaultsTest, CardsDisplay_CardLang_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);