diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.cpp b/cockatrice/src/game_graphics/board/abstract_card_item.cpp index 9cd0371c9..f7ae1ff71 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.cpp +++ b/cockatrice/src/game_graphics/board/abstract_card_item.cpp @@ -13,6 +13,7 @@ #include "libcockatrice/card/printing/exact_card.h" #include "libcockatrice/utility/card_ref.h" +#include #include #include #include @@ -47,13 +48,16 @@ class QSizeF; // Width the hovered card smooth-zooms to: exactly the hover panel picture size, so // all four corners coincide when the panel swaps in over it. static constexpr qreal HOVER_ZOOM_WIDTH = 270.0; +// The pre-panel hover enlargement. The smooth zoom is opt-in, so this stays the hover +// behavior whenever no zoom trigger is configured (see setHoverHighlighted). +static constexpr qreal LEGACY_HOVER_SCALE = 1.1; static constexpr int HOVER_ZOOM_IN_MS = 220; static constexpr int HOVER_ZOOM_OUT_MS = 140; static constexpr int FADE_DURATION_MS = 140; AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id) : ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0), - bgColor(Qt::transparent), isHovered(false), realZValue(0) + bgColor(Qt::transparent), isHovered(false), isHoverHighlighted(false), realZValue(0) { setCursor(Qt::OpenHandCursor); setFlag(ItemIsSelectable); @@ -79,7 +83,9 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef // so siblings and the clip container cannot cut through the shrinking card. connect(hoverZoom, &QPropertyAnimation::finished, this, [this] { if (!isHovered) { - setZValue(realZValue); + // The cursor may still rest on the card (e.g. wheel-up reverts the zoom), + // in which case the highlight keeps it raised. + setZValue(isHoverHighlighted ? ZValues::HOVERED_CARD : realZValue); // Final repaint at the settled footprint: with no scale delta the animation // emits no per-frame updates, and zones would keep a stale clip hole behind. if (scene()) { @@ -164,7 +170,7 @@ void AbstractCardItem::setRealZValue(qreal _zValue) // During hover, zValue is overridden to HOVERED_CARD. Layout operations // like reorganizeCards() call setRealZValue() on all cards including the // hovered one — skip setZValue() here to avoid clobbering the override. - if (!isHovered) { + if (!isHovered && !isHoverHighlighted) { setZValue(_zValue); } } @@ -262,9 +268,9 @@ void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * painter->setRenderHint(QPainter::Antialiasing, false); - if (isSelected() || isHovered) { + if (isSelected() || isHovered || isHoverHighlighted) { QPen pen; - if (isHovered) { + if (isHovered || isHoverHighlighted) { pen.setColor(Qt::yellow); } if (isSelected()) { @@ -334,6 +340,47 @@ void AbstractCardItem::setHovered(bool _hovered) update(); } +void AbstractCardItem::setHoverHighlighted(bool highlighted) +{ + if (isHoverHighlighted == highlighted) { + return; + } + + if (highlighted) { + // The card-info widget listens for this: hovering still updates it right away. + processHoverEvent(); + } + + isHoverHighlighted = highlighted; + if (highlighted) { + setZValue(ZValues::HOVERED_CARD); + } + + // Fallback to the historical hover scale: the smooth hover zoom is opt-in and its + // trigger defaults to Off, so when hovering drives neither the zoom nor the panel + // (zoom trigger Off, panel not on hover) contact alone must still enlarge the card + // to the old 1.1x while scaleCards is on. Otherwise every existing user silently + // loses the hover enlargement on upgrade. + auto &display = SettingsCache::instance().cardsDisplay(); + const bool legacyHoverScale = display.getScaleCards() && + display.getHoverZoomTrigger() == CardsDisplaySettings::HoverPanelTriggerOff && + display.getHoverPanelTrigger() != CardsDisplaySettings::HoverPanelTriggerHover; + if (legacyHoverScale) { + // Centered like the smooth zoom, so growing and shrinking never drift. + setTransformOriginPoint(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F); + hoverZoom->stop(); + hoverZoom->setStartValue(scale()); + hoverZoom->setEndValue(highlighted ? LEGACY_HOVER_SCALE : 1.0); + hoverZoom->setDuration(highlighted ? HOVER_ZOOM_IN_MS : HOVER_ZOOM_OUT_MS); + hoverZoom->start(); + } else if (!highlighted && !isHovered && hoverZoom->state() != QAbstractAnimation::Running) { + // A zoomed card keeps its raised z until the shrink-out finishes (its finished + // handler restores it); a highlight-only card has nothing to unwind. + setZValue(realZValue); + } + update(); +} + void AbstractCardItem::setColor(const QString &_color) { state->setColor(_color); diff --git a/cockatrice/src/game_graphics/board/abstract_card_item.h b/cockatrice/src/game_graphics/board/abstract_card_item.h index ddedbb163..82058f08a 100644 --- a/cockatrice/src/game_graphics/board/abstract_card_item.h +++ b/cockatrice/src/game_graphics/board/abstract_card_item.h @@ -37,6 +37,7 @@ protected: private: bool isHovered; + bool isHoverHighlighted; qreal realZValue; bool pendingSelectionCollapse; int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings @@ -134,6 +135,16 @@ public: } void setRealZValue(qreal _zValue); void setHovered(bool _hovered); + /** + * @brief Shows the hover outline and stacking lift, plus the historical 1.1x hover + * scale whenever no zoom trigger is configured. + * + * The board zoom is trigger-gated; the hover feedback (card info, outline, raised + * stacking) still follows the cursor immediately on contact. With the zoom trigger + * off and the panel not on hover, the old 1.1x enlargement (gated by scaleCards) + * stays in effect so upgrading does not silently drop it. + */ + void setHoverHighlighted(bool highlighted); qreal getCardScale() const; void setCardScale(qreal scale); qreal getCardOpacity() const; diff --git a/cockatrice/src/game_graphics/game_scene.cpp b/cockatrice/src/game_graphics/game_scene.cpp index e87411bb0..cf9c2dd35 100644 --- a/cockatrice/src/game_graphics/game_scene.cpp +++ b/cockatrice/src/game_graphics/game_scene.cpp @@ -268,15 +268,17 @@ void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screen } // The zoom and panel triggers are independently bindable: a right-click fires - // whichever phases listen to it. Once right-click drives either phase the context - // menu stays suppressed — popping it would steal the flow (and the panel already - // carries every action when it is the panel trigger). + // whichever phases listen to it. When the panel is the trigger it carries every + // action itself, so the context menu stays suppressed; when right-click only zooms, + // the menu still opens, since nothing else would summon it. auto &display = SettingsCache::instance().cardsDisplay(); const bool zoomFired = display.getHoverZoomTrigger() == CardsDisplaySettings::HoverPanelTriggerRightClick; const bool panelFired = display.getHoverPanelTrigger() == CardsDisplaySettings::HoverPanelTriggerRightClick; if (zoomFired || panelFired) { fireTriggers(card, zoomFired, panelFired); - return; + if (panelFired) { + return; + } } // Hide the hover popup when the context menu opens @@ -843,6 +845,17 @@ void GameScene::updateHoveredCard(CardItem *newCard) hoveredCard = nullptr; } + // The hover outline and the card info follow the cursor on contact: only the board + // zoom and the panel wait for their trigger. A highlight-only card has no zoom to + // unwind, so it is dropped directly instead of through endCardHover(). + if (hoverHighlightCard && hoverHighlightCard != newCard) { + hoverHighlightCard->setHoverHighlighted(false); + } + hoverHighlightCard = newCard; + if (newCard) { + newCard->setHoverHighlighted(true); + } + auto &display = SettingsCache::instance().cardsDisplay(); const int zoomTrigger = display.getHoverZoomTrigger(); const int panelTrigger = display.getHoverPanelTrigger(); diff --git a/cockatrice/src/game_graphics/game_scene.h b/cockatrice/src/game_graphics/game_scene.h index addff79c0..9c8c422a3 100644 --- a/cockatrice/src/game_graphics/game_scene.h +++ b/cockatrice/src/game_graphics/game_scene.h @@ -78,8 +78,11 @@ private: ArrowRegistry arrowRegistry; ///< ID registry for arrow graphics items QList zoneViews; ///< Active zone view widgets QSize viewSize; ///< Current view size - QPointer hoveredCard; ///< Currently hovered card - QPointer activeCard; ///< Card the currently open context menu targets + QPointer hoveredCard; ///< Currently hovered card (zoomed) + /// Card the cursor is resting on. It carries the hover outline and the card-info + /// update immediately, independently of the zoom/panel triggers. + QPointer hoverHighlightCard; + QPointer activeCard; ///< Card the currently open context menu targets /// Card armed for double-confirm panel summon (first trigger zooms, repeat shows). QPointer confirmArmedCard; /// Ancestor chain of the hovered card with pre-hover Z-values. Lifted while hovering diff --git a/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp b/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp index b7d236b15..18da1f4b7 100644 --- a/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp @@ -284,9 +284,8 @@ void CardHoverWidget::createNameBarOverlay() if (!nameEdit->isVisible()) { return; } - if (currentCard && currentCard->getOwner()) { - auto *actions = currentCard->getOwner()->getPlayerActions(); - if (actions) { + if (canModifyCurrentCard()) { + if (auto *actions = currentCard->getOwner()->getPlayerActions()) { QList cardList{currentCard->getState()}; actions->actSetAnnotation(cardList, nameEdit->text()); } @@ -771,6 +770,23 @@ void CardHoverWidget::onCardDestroyed() // ---------- Grouped action builders (mirror CardMenu per zone) ---------- +bool CardHoverWidget::canModifyCurrentCard() const +{ + if (!currentCard || !currentCard->getOwner() || !currentPlayer) { + return false; + } + if (!currentPlayer->getLogic()->getPlayerInfo()->getLocalOrJudge()) { + return false; + } + // A revealed zone we cannot write to is read-only even for its owner. + if (auto *view = qobject_cast(currentCard->getZone())) { + if (view->getRevealZone() && !view->getWriteableRevealZone()) { + return false; + } + } + return true; +} + void CardHoverWidget::rebuildActionsForZone() { clearLayout(mainPageLayout); @@ -785,12 +801,7 @@ void CardHoverWidget::rebuildActionsForZone() auto *card = currentCard.data(); QList cardList{card->getState()}; - bool canModify = currentPlayer->getLogic()->getPlayerInfo()->getLocalOrJudge(); - if (auto *view = qobject_cast(card->getZone())) { - if (view->getRevealZone() && !view->getWriteableRevealZone()) { - canModify = false; - } - } + const bool canModify = canModifyCurrentCard(); QString zoneName = card->getZone() ? card->getZone()->getName() : QString(); @@ -1175,6 +1186,11 @@ void CardHoverWidget::setPTExpanded(bool expanded) if (expanded && (!currentCard || currentCard->getPT().isEmpty())) { expanded = false; } + // The buttons act on our own copy of the card by id, so on another player's card they + // would edit our zone. Offer them only where the action tabs allow edits. + if (expanded && !canModifyCurrentCard()) { + expanded = false; + } // Mouse movement drives this on every event; only apply a real state change. if (ptExpanded == expanded) { return; @@ -1558,8 +1574,9 @@ bool CardHoverWidget::isClosing() const bool CardHoverWidget::eventFilter(QObject *obj, QEvent *event) { - // Handle double-click on name label to start editing annotation - if (obj == nameLabel && event->type() == QEvent::MouseButtonDblClick) { + // Handle double-click on name label to start editing annotation. Annotations are set + // by card id on our own zone, so another player's card must not open the editor. + if (obj == nameLabel && event->type() == QEvent::MouseButtonDblClick && canModifyCurrentCard()) { nameLabel->setVisible(false); annotationLabel->setVisible(false); nameEdit->setVisible(true); diff --git a/cockatrice/src/interface/widgets/cards/card_hover_widget.h b/cockatrice/src/interface/widgets/cards/card_hover_widget.h index 05c0d3e75..b2d098441 100644 --- a/cockatrice/src/interface/widgets/cards/card_hover_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_hover_widget.h @@ -174,6 +174,8 @@ private: void scheduleRefresh(); void rebuildActionsForZone(); + /** @brief Whether the local player may edit the current card (own card, judge, writable zone). */ + bool canModifyCurrentCard() const; void buildMainTab(const QList &cardList, const QString &zoneName, bool canModify); void buildMoveTab(const QList &cardList, const QString &zoneName, bool canModify); void buildCounterTab(const QList &cardList, const QString &zoneName, bool canModify); 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 ed8fb415d..b29c4d90c 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 @@ -33,7 +33,7 @@ enum visualDeckStoragePromptForConversionIndex static int clampHoverTrigger(int trigger) { return qBound(static_cast(CardsDisplaySettings::HoverPanelTriggerHover), trigger, - static_cast(CardsDisplaySettings::HoverPanelTriggerShortcut)); + static_cast(CardsDisplaySettings::HoverPanelTriggerOff)); } UserInterfaceSettingsPage::UserInterfaceSettingsPage() @@ -167,6 +167,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() 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(), @@ -176,6 +177,7 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() 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(), @@ -426,9 +428,13 @@ void UserInterfaceSettingsPage::updateHoverPanelUiState() // both phases share one trigger (repeating it). const auto hoverTrigger = CardsDisplaySettings::HoverPanelTriggerHover; const auto wheelTrigger = CardsDisplaySettings::HoverPanelTriggerWheel; + const auto offTrigger = CardsDisplaySettings::HoverPanelTriggerOff; const bool zoomOnHover = hoverZoomTriggerSelector.currentIndex() == hoverTrigger; const bool panelOnHover = hoverPanelTriggerSelector.currentIndex() == hoverTrigger; - const bool sharedTrigger = hoverZoomTriggerSelector.currentIndex() == hoverPanelTriggerSelector.currentIndex(); + const bool anyOff = + hoverZoomTriggerSelector.currentIndex() == offTrigger || hoverPanelTriggerSelector.currentIndex() == offTrigger; + const bool sharedTrigger = + !anyOff && hoverZoomTriggerSelector.currentIndex() == hoverPanelTriggerSelector.currentIndex(); hoverZoomDelayLabel.setVisible(zoomOnHover); hoverZoomDelayBox.setVisible(zoomOnHover); hoverPanelDelayLabel.setVisible(panelOnHover); @@ -480,6 +486,7 @@ void UserInterfaceSettingsPage::retranslateUi() selector->setItemText(CardsDisplaySettings::HoverPanelTriggerRightClick, tr("Right-click")); selector->setItemText(CardsDisplaySettings::HoverPanelTriggerWheel, tr("Mouse wheel")); selector->setItemText(CardsDisplaySettings::HoverPanelTriggerShortcut, tr("Keyboard shortcut")); + selector->setItemText(CardsDisplaySettings::HoverPanelTriggerOff, tr("Off")); } hoverPanelDoubleConfirmCheckBox.setText(tr("Zoom first; repeat the action to show the panel")); hoverZoomDelayLabel.setText(tr("Time before the zoom kicks in:")); diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp index 9283cde0d..910e3f09d 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.cpp @@ -70,12 +70,16 @@ bool CardsDisplaySettings::getScaleCards() const int CardsDisplaySettings::getHoverPanelTrigger() const { - return getValue("hoverPanelTrigger", QString(), QString(), 0).toInt(); + // Off by default: the panel is new, so it is opt-in. + return getValue("hoverPanelTrigger", QString(), QString(), static_cast(HoverPanelTriggerOff)).toInt(); } int CardsDisplaySettings::getHoverZoomTrigger() const { - return getValue("hoverZoomTrigger", QString(), QString(), 0).toInt(); + // Off by default: the smooth hover zoom is opt-in. With no trigger configured, + // hovering keeps the historical 1.1x scale (AbstractCardItem::setHoverHighlighted) + // instead of the smooth zoom to the hover panel size. + return getValue("hoverZoomTrigger", QString(), QString(), static_cast(HoverPanelTriggerOff)).toInt(); } bool CardsDisplaySettings::getHoverPanelDoubleConfirm() const diff --git a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h index 1ccf2675d..59a513e8f 100644 --- a/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/cards_display_settings.h @@ -22,7 +22,9 @@ public: HoverPanelTriggerHover = 0, HoverPanelTriggerRightClick = 1, HoverPanelTriggerWheel = 2, - HoverPanelTriggerShortcut = 3 + HoverPanelTriggerShortcut = 3, + /// Never fires. The hover zoom and panel are opt-in, so this is their default. + HoverPanelTriggerOff = 4 }; [[nodiscard]] bool getDisplayCardNames() const override; diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 0de264a97..4ca440567 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -686,7 +686,8 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default) TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelTrigger_Default) { CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHoverPanelTrigger(), 0); + // Off by default: the panel is opt-in on upgrade. + ASSERT_EQ(s.getHoverPanelTrigger(), CardsDisplaySettings::HoverPanelTriggerOff); } TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelTrigger_SetAndGet) @@ -701,7 +702,8 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_HoverPanelTrigger_SetAndGet) TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomTrigger_Default) { CardsDisplaySettings s(settingsPath, nullptr); - ASSERT_EQ(s.getHoverZoomTrigger(), 0); + // Off by default: the smooth hover zoom is opt-in on upgrade. + ASSERT_EQ(s.getHoverZoomTrigger(), CardsDisplaySettings::HoverPanelTriggerOff); } TEST_F(SettingsDefaultsTest, CardsDisplay_HoverZoomTrigger_SetAndGet)