diff --git a/cockatrice/src/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index 24790a407..202b39247 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -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)}, 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 44ce01aba..ed8fb415d 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 @@ -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(CardsDisplaySettings::HoverPanelTriggerHover), trigger, + static_cast(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(&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(&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")); 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 297d80c35..6b60c58bf 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 @@ -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; 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 a5ab4fc37..8eb81d2bc 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_cards_display_settings_provider.h @@ -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; diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index f047973f0..9283cde0d 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -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"); diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 961550d3c..1ccf2675d 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -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); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index aea7f1d54..0de264a97 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -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);