mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-10 20:13:24 -08:00
[Game] Wire hover zoom/panel triggers through the scene
This commit is contained in:
parent
b2f0528fc7
commit
abbab61e4f
6 changed files with 609 additions and 13 deletions
|
|
@ -1,6 +1,7 @@
|
|||
#include "game_scene.h"
|
||||
|
||||
#include "../client/settings/cache_settings.h"
|
||||
#include "../interface/widgets/cards/card_hover_widget.h"
|
||||
#include "../interface/theme_manager.h"
|
||||
#include "animated_item.h"
|
||||
#include "board/abstract_card_item.h"
|
||||
|
|
@ -11,16 +12,20 @@
|
|||
#include "player/menu/player_menu.h"
|
||||
#include "player/player_graphics_item.h"
|
||||
#include "player/player_target.h"
|
||||
#include "z_values.h"
|
||||
#include "zones/card_zone.h"
|
||||
#include "zones/select_zone.h"
|
||||
#include "zones/view_zone.h"
|
||||
#include "zones/view_zone_widget.h"
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QCursor>
|
||||
#include <QDebug>
|
||||
#include <QEvent>
|
||||
#include <QGraphicsItem>
|
||||
#include <QGraphicsObject>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsSceneWheelEvent>
|
||||
#include <QGraphicsView>
|
||||
#include <QListIterator>
|
||||
#include <QMenu>
|
||||
|
|
@ -29,9 +34,11 @@
|
|||
#include <QObject>
|
||||
#include <QPoint>
|
||||
#include <QRectF>
|
||||
#include <QScrollBar>
|
||||
#include <QSet>
|
||||
#include <QSharedPointer>
|
||||
#include <QtMath>
|
||||
#include <QTimer>
|
||||
#include <QtGlobal>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <libcockatrice/game/abstract_game.h>
|
||||
|
|
@ -43,6 +50,7 @@
|
|||
#include <libcockatrice/game/player/player_logic.h>
|
||||
#include <libcockatrice/game/zones/card_zone_logic.h>
|
||||
#include <libcockatrice/game/zones/view_zone_logic.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/zone_names.h>
|
||||
#include <numeric>
|
||||
|
|
@ -75,6 +83,22 @@ GameScene::GameScene(PhasesToolbar *_phasesToolbar, QObject *parent)
|
|||
}
|
||||
});
|
||||
|
||||
// Card hover popup (cardHoverWidget is created lazily on first show)
|
||||
hoverActivateTimer = new QTimer(this);
|
||||
hoverActivateTimer->setSingleShot(true);
|
||||
hoverActivateTimer->setInterval(500);
|
||||
connect(hoverActivateTimer, &QTimer::timeout, this, &GameScene::onHoverActivateTimeout);
|
||||
|
||||
hoverPanelTimer = new QTimer(this);
|
||||
hoverPanelTimer->setSingleShot(true);
|
||||
hoverPanelTimer->setInterval(200);
|
||||
connect(hoverPanelTimer, &QTimer::timeout, this, &GameScene::onHoverPanelTimeout);
|
||||
|
||||
hoverGraceTimer = new QTimer(this);
|
||||
hoverGraceTimer->setSingleShot(true);
|
||||
hoverGraceTimer->setInterval(200);
|
||||
connect(hoverGraceTimer, &QTimer::timeout, this, &GameScene::onHoverGraceTimeout);
|
||||
|
||||
rearrange();
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +117,9 @@ GameScene::~GameScene()
|
|||
|
||||
delete animationTimer;
|
||||
animationTimer = nullptr;
|
||||
delete hoverActivateTimer;
|
||||
delete hoverPanelTimer;
|
||||
delete hoverGraceTimer;
|
||||
|
||||
// Delete all ArrowItems before QGraphicsScene's base destructor runs.
|
||||
// QGraphicsScene::~QGraphicsScene() destroys items in arbitrary order.
|
||||
|
|
@ -239,6 +266,21 @@ void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screen
|
|||
return;
|
||||
}
|
||||
|
||||
// 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).
|
||||
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;
|
||||
}
|
||||
|
||||
// Hide the hover popup when the context menu opens
|
||||
hideCardHoverPopup();
|
||||
|
||||
setActiveCard(card);
|
||||
|
||||
if (auto *menu = view->getPlayerMenu()->updateCardMenu(card->getState())) {
|
||||
|
|
@ -690,7 +732,6 @@ void GameScene::requestArrowDeletion(int playerId, int arrowId)
|
|||
emit arrowDeletionRequested(playerId, arrowId);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::onCardZoneChanged(CardState *card, bool sameZone)
|
||||
{
|
||||
auto *item = qobject_cast<CardItem *>(card->parent());
|
||||
|
|
@ -710,6 +751,11 @@ void GameScene::onCardZoneChanged(CardState *card, bool sameZone)
|
|||
for (auto *arrow : toDelete) {
|
||||
deleteArrow(arrow->getCreatorId(), arrow->getId());
|
||||
}
|
||||
|
||||
// The hover panel stays open on a moved card: follow it instead of stranding.
|
||||
if (!sameZone && cardHoverWidget && cardHoverWidget->getCardItem() == item) {
|
||||
positionCardHoverPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner)
|
||||
|
|
@ -746,27 +792,129 @@ void GameScene::clearArrowsForPlayerLocally(int playerId)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Hover Handling ----------
|
||||
|
||||
CardItem *GameScene::getHoveredCard() const
|
||||
{
|
||||
return hoveredCard;
|
||||
}
|
||||
|
||||
void GameScene::updateHover(const QPointF &scenePos)
|
||||
{
|
||||
auto itemList = items(scenePos, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder, getViewTransform());
|
||||
updateHoveredCard(pickCardAt(scenePos));
|
||||
}
|
||||
|
||||
CardItem *GameScene::pickCardAt(const QPointF &scenePos)
|
||||
{
|
||||
auto itemList = items(scenePos, Qt::IntersectsItemBoundingRect, Qt::DescendingOrder, getViewTransform());
|
||||
CardZone *zone = findTopmostZone(itemList);
|
||||
CardItem *topCard = zone ? findTopmostCardInZone(itemList, zone) : nullptr;
|
||||
updateHoveredCard(topCard);
|
||||
if (!topCard && hoveredCard) {
|
||||
const QRectF guard = hoveredCard->sceneBoundingRect().adjusted(-8, -8, 8, 8);
|
||||
if (guard.contains(scenePos)) {
|
||||
// The zoomed card overflows onto neighboring zones, where naive topmost-zone
|
||||
// picking attributes the cursor to the zone underneath. While the cursor is
|
||||
// still over the zoomed footprint (with hysteresis for edge jiggle), keep it.
|
||||
// A genuinely different topmost card still takes over.
|
||||
return hoveredCard;
|
||||
}
|
||||
}
|
||||
return topCard;
|
||||
}
|
||||
|
||||
void GameScene::updateHoveredCard(CardItem *newCard)
|
||||
{
|
||||
// Safety: if the popup's card was destroyed, hide the popup immediately
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && !cardHoverWidget->hasCard()) {
|
||||
hideCardHoverPopup();
|
||||
}
|
||||
// The hovered card may have been destroyed without ending the hover: its QPointer
|
||||
// nulled itself, but the ancestor lift is still outstanding. Restore it now.
|
||||
if (!hoveredCard && !hoverLiftedAncestors.isEmpty()) {
|
||||
restoreHoverLift();
|
||||
}
|
||||
|
||||
// Hovering alone never starts anything anymore: the trigger (dwell, click, wheel,
|
||||
// shortcut) resolves first, then the zoom follows. This keeps the feature calm.
|
||||
bool wasHoveringCard = (hoveredCard != nullptr);
|
||||
if (hoveredCard && (newCard != hoveredCard)) {
|
||||
endCardHover(hoveredCard);
|
||||
hoveredCard = nullptr;
|
||||
}
|
||||
if (newCard && (newCard != hoveredCard)) {
|
||||
beginCardHover(newCard);
|
||||
|
||||
auto &display = SettingsCache::instance().cardsDisplay();
|
||||
const int zoomTrigger = display.getHoverZoomTrigger();
|
||||
const int panelTrigger = display.getHoverPanelTrigger();
|
||||
const bool doubleConfirm = display.getHoverPanelDoubleConfirm() && zoomTrigger == panelTrigger;
|
||||
|
||||
if (newCard) {
|
||||
// Cancel any pending grace timer (mouse is back on a card)
|
||||
hoverGracePending = false;
|
||||
hoverGraceTimer->stop();
|
||||
|
||||
if (newCard == hoveredCard) {
|
||||
// Steady state: zoom already up. Refresh an open panel, otherwise nothing —
|
||||
// in particular this must not re-fire an armed double-confirm.
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->getCardItem() == newCard) {
|
||||
cardHoverWidget->refreshDisplay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->getCardItem() == newCard) {
|
||||
// Panel open but zoom dropped (e.g. survived a Leave) — zoom back and refresh.
|
||||
beginCardHover(newCard);
|
||||
hoveredCard = newCard;
|
||||
cardHoverWidget->refreshDisplay();
|
||||
return;
|
||||
}
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible()) {
|
||||
// Panel open elsewhere — move the zoom at once so it never strands, but let
|
||||
// the panel follow after the configured delay instead of strobing open on
|
||||
// every card swept across.
|
||||
beginCardHover(newCard);
|
||||
hoveredCard = newCard;
|
||||
confirmArmedCard.clear();
|
||||
hoverPanelTarget = newCard;
|
||||
hoverPanelTimer->start(display.getHoverPanelDelayMs());
|
||||
return;
|
||||
}
|
||||
if (zoomTrigger != CardsDisplaySettings::HoverPanelTriggerHover &&
|
||||
panelTrigger != CardsDisplaySettings::HoverPanelTriggerHover) {
|
||||
// Mouse contact alone drives neither phase in this config.
|
||||
return;
|
||||
}
|
||||
if (doubleConfirm && confirmArmedCard == newCard) {
|
||||
// Repeat hover on an armed card: zoom and show at once, no second dwell.
|
||||
beginCardHover(newCard);
|
||||
hoveredCard = newCard;
|
||||
confirmArmedCard.clear();
|
||||
showCardHoverPopup(newCard);
|
||||
return;
|
||||
}
|
||||
if (newCard != hoverPopupTarget) {
|
||||
// First contact (or a new card): dwell, then resolve on timeout.
|
||||
hoverPopupTarget = newCard;
|
||||
hoverActivateTimer->start(display.getHoverZoomDelayMs());
|
||||
}
|
||||
} else {
|
||||
// No card hovered: drop any pending dwell/panel timers (kills stale shows) and start
|
||||
// the grace timer only when the mouse just left a card with the panel open.
|
||||
hoverPopupTarget = nullptr;
|
||||
hoverPanelTarget = nullptr;
|
||||
hoverActivateTimer->stop();
|
||||
hoverPanelTimer->stop();
|
||||
// Non-hover zoom arms only make sense while approaching the card: leaving
|
||||
// re-arms the zoom for the next explicit trigger instead of showing the panel.
|
||||
// (Hover-trigger arms survive the leave so re-entering completes the repeat.)
|
||||
if (zoomTrigger != CardsDisplaySettings::HoverPanelTriggerHover) {
|
||||
confirmArmedCard.clear();
|
||||
}
|
||||
if (wasHoveringCard && cardHoverWidget && cardHoverWidget->isVisible() &&
|
||||
!cardHoverWidget->isMouseOverWidget() && !hoverGracePending) {
|
||||
hoverGracePending = true;
|
||||
hoverGraceTimer->start();
|
||||
}
|
||||
}
|
||||
hoveredCard = newCard;
|
||||
}
|
||||
|
||||
void GameScene::beginCardHover(CardItem *card)
|
||||
|
|
@ -775,16 +923,317 @@ void GameScene::beginCardHover(CardItem *card)
|
|||
if (auto *zone = SelectZone::findOwningSelectZone(card)) {
|
||||
zone->escapeClipForHover(card);
|
||||
}
|
||||
// Lift the whole ancestor chain above their siblings: the zoomed card overflows its
|
||||
// own zone, and Qt orders whole subtrees level by level, so no single z-value wins
|
||||
// every comparison on its own. Harmless elsewhere (areas and zones tile without
|
||||
// overlapping; arrows, zone views and drag items keep their higher layers).
|
||||
// Restore first: if a previous hover never ended (its card was destroyed mid-hover),
|
||||
// its chain is still outstanding and must land before a new one is recorded.
|
||||
restoreHoverLift();
|
||||
QGraphicsItem *ancestor = card->parentItem();
|
||||
while (ancestor) {
|
||||
// The chain is QGraphicsObject-derived in practice; a plain QGraphicsItem
|
||||
// ancestor cannot be guarded, so it keeps its z-value instead of risking an
|
||||
// unrestorable lift.
|
||||
if (QGraphicsObject *ancestorObject = ancestor->toGraphicsObject()) {
|
||||
hoverLiftedAncestors.append({QPointer<QGraphicsObject>(ancestorObject), ancestor->zValue()});
|
||||
ancestor->setZValue(ZValues::HOVERED_AREA);
|
||||
}
|
||||
ancestor = ancestor->parentItem();
|
||||
}
|
||||
liftOwner = card;
|
||||
connect(card, &AbstractCardItem::hoverShrinkFinished, this, &GameScene::onHoverShrinkFinished,
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
|
||||
void GameScene::endCardHover(CardItem *card)
|
||||
{
|
||||
if (auto *zone = SelectZone::findOwningSelectZone(card)) {
|
||||
zone->restoreClipAfterHover(card);
|
||||
}
|
||||
// Only the zoom-out starts here: the z-value, the clip escape and the ancestor
|
||||
// lift stay put until the shrink finishes (onHoverShrinkFinished), so siblings and
|
||||
// the clip container cannot cut through the still-enlarged card.
|
||||
card->setHovered(false);
|
||||
}
|
||||
|
||||
void GameScene::onHoverShrinkFinished(AbstractCardItem *card)
|
||||
{
|
||||
auto *cardItem = qobject_cast<CardItem *>(card);
|
||||
if (!cardItem) {
|
||||
return;
|
||||
}
|
||||
// Per-card and idempotent: always settle, even if a newer hover owns the lift.
|
||||
if (auto *zone = SelectZone::findOwningSelectZone(cardItem)) {
|
||||
zone->restoreClipAfterHover(cardItem);
|
||||
}
|
||||
if (liftOwner != cardItem) {
|
||||
// A newer hover already owns the lift (or nothing does): leave it alone.
|
||||
return;
|
||||
}
|
||||
restoreHoverLift();
|
||||
}
|
||||
|
||||
void GameScene::restoreHoverLift()
|
||||
{
|
||||
for (const auto &[lifted, zValue] : hoverLiftedAncestors) {
|
||||
if (lifted) {
|
||||
lifted->setZValue(zValue);
|
||||
}
|
||||
}
|
||||
hoverLiftedAncestors.clear();
|
||||
liftOwner.clear();
|
||||
}
|
||||
|
||||
// ---------- Card Hover Popup ----------
|
||||
|
||||
void GameScene::showCardHoverPopup(CardItem *card)
|
||||
{
|
||||
if (!card || !card->getOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerGraphicsItem *playerView = playerViews.value(card->getOwner()->getPlayerInfo()->getId());
|
||||
if (!playerView) {
|
||||
return;
|
||||
}
|
||||
|
||||
hoverActivateTimer->stop();
|
||||
hoverPanelTimer->stop();
|
||||
hoverGracePending = false;
|
||||
hoverGraceTimer->stop();
|
||||
hoverPopupTarget = card;
|
||||
hoverPanelTarget = nullptr;
|
||||
confirmArmedCard.clear();
|
||||
|
||||
if (!cardHoverWidget) {
|
||||
auto *viewport = views().isEmpty() ? nullptr : views().first()->viewport();
|
||||
cardHoverWidget = new CardHoverWidget(viewport);
|
||||
connect(cardHoverWidget, &CardHoverWidget::hoverEntered, this, &GameScene::onHoverWidgetEntered);
|
||||
connect(cardHoverWidget, &CardHoverWidget::hoverLeft, this, &GameScene::onHoverWidgetLeft);
|
||||
|
||||
// Keep the popup anchored to its card while the view scrolls, zooms, or resizes
|
||||
if (!views().isEmpty()) {
|
||||
auto *view = views().first();
|
||||
connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, this, &GameScene::positionCardHoverPopup);
|
||||
connect(view->verticalScrollBar(), &QScrollBar::valueChanged, this, &GameScene::positionCardHoverPopup);
|
||||
if (viewport) {
|
||||
viewport->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crossfade: the opaque panel picture sits exactly over the zoomed card, so fade
|
||||
// the board card out underneath it (masks any residual size/corner mismatch) and
|
||||
// fade any previously shown card back in.
|
||||
CardItem *oldCard = cardHoverWidget->hasCard() ? cardHoverWidget->getCardItem() : nullptr;
|
||||
|
||||
cardHoverWidget->setCard(card, playerView);
|
||||
positionCardHoverPopup();
|
||||
cardHoverWidget->showAnimated();
|
||||
|
||||
if (oldCard && oldCard != card) {
|
||||
oldCard->fadeCardIn();
|
||||
}
|
||||
card->fadeCardOut();
|
||||
}
|
||||
|
||||
void GameScene::hideCardHoverPopup()
|
||||
{
|
||||
// Hiding is terminal: drop all pending hover timing with it, or a dwell/panel
|
||||
// timer that resolves afterwards pops the panel back up (e.g. over the context
|
||||
// menu that hideCardHoverPopup was just called for).
|
||||
hoverActivateTimer->stop();
|
||||
hoverPanelTimer->stop();
|
||||
hoverGracePending = false;
|
||||
hoverGraceTimer->stop();
|
||||
if (cardHoverWidget) {
|
||||
// The panel is gone: whatever card it covered must come back.
|
||||
if (CardItem *coveredCard = cardHoverWidget->getCardItem()) {
|
||||
coveredCard->fadeCardIn();
|
||||
}
|
||||
if (cardHoverWidget->isVisible()) {
|
||||
cardHoverWidget->hideAnimated();
|
||||
}
|
||||
}
|
||||
hoverPopupTarget = nullptr;
|
||||
hoverPanelTarget = nullptr;
|
||||
}
|
||||
|
||||
void GameScene::positionCardHoverPopup()
|
||||
{
|
||||
if (!cardHoverWidget || !cardHoverWidget->hasCard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto *viewport = views().isEmpty() ? nullptr : views().first()->viewport();
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRectF sceneRect = cardHoverWidget->getCardItem()->sceneBoundingRect();
|
||||
QTransform vpTransform = getViewportTransform();
|
||||
cardHoverWidget->positionNextTo(sceneRect, vpTransform, viewport);
|
||||
}
|
||||
|
||||
void GameScene::onHoverActivateTimeout()
|
||||
{
|
||||
// Dwell resolved: the hover drives whichever phases listen to it. The cursor must
|
||||
// still be over the pending card (rect test, not a fresh pick, so pick flakiness
|
||||
// cannot veto it); otherwise this was a stale timer.
|
||||
CardItem *target = hoverPopupTarget;
|
||||
hoverPopupTarget = nullptr;
|
||||
if (!target || views().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto *view = views().first();
|
||||
const QPointF cursorScene = view->mapToScene(view->viewport()->mapFromGlobal(QCursor::pos()));
|
||||
if (!target->sceneBoundingRect().contains(cursorScene)) {
|
||||
return;
|
||||
}
|
||||
auto &display = SettingsCache::instance().cardsDisplay();
|
||||
const bool zoomPhase = display.getHoverZoomTrigger() == CardsDisplaySettings::HoverPanelTriggerHover;
|
||||
const bool panelPhase = display.getHoverPanelTrigger() == CardsDisplaySettings::HoverPanelTriggerHover;
|
||||
const bool effectiveDouble =
|
||||
display.getHoverPanelDoubleConfirm() && display.getHoverZoomTrigger() == display.getHoverPanelTrigger();
|
||||
if (hoveredCard && hoveredCard != target) {
|
||||
endCardHover(hoveredCard);
|
||||
}
|
||||
if (zoomPhase) {
|
||||
if (hoveredCard != target) {
|
||||
beginCardHover(target);
|
||||
hoveredCard = target;
|
||||
}
|
||||
if (effectiveDouble) {
|
||||
confirmArmedCard = target;
|
||||
}
|
||||
}
|
||||
if (panelPhase && !effectiveDouble) {
|
||||
hoverPanelTarget = target;
|
||||
hoverPanelTimer->start(display.getHoverPanelDelayMs());
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::onHoverPanelTimeout()
|
||||
{
|
||||
// Panel delay resolved: show, but only if the target card is still the one under
|
||||
// the cursor (same cheap rect check — no dependence on pick stability).
|
||||
CardItem *target = hoverPanelTarget;
|
||||
hoverPanelTarget = nullptr;
|
||||
if (!target || views().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto *view = views().first();
|
||||
const QPointF cursorScene = view->mapToScene(view->viewport()->mapFromGlobal(QCursor::pos()));
|
||||
if (!target->sceneBoundingRect().contains(cursorScene)) {
|
||||
return;
|
||||
}
|
||||
ensureZoomed(target);
|
||||
showCardHoverPopup(target);
|
||||
}
|
||||
|
||||
void GameScene::onHoverGraceTimeout()
|
||||
{
|
||||
hoverGracePending = false;
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->isMouseOverWidget()) {
|
||||
return;
|
||||
}
|
||||
hideCardHoverPopup();
|
||||
}
|
||||
|
||||
void GameScene::summonHoverPanel()
|
||||
{
|
||||
// Summon for whatever is under the cursor right now; hovering is not required.
|
||||
if (views().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto *view = views().first();
|
||||
const QPointF cursorScene = view->mapToScene(view->viewport()->mapFromGlobal(QCursor::pos()));
|
||||
if (CardItem *card = pickCardAt(cursorScene)) {
|
||||
auto &display = SettingsCache::instance().cardsDisplay();
|
||||
fireTriggers(card, display.getHoverZoomTrigger() == CardsDisplaySettings::HoverPanelTriggerShortcut,
|
||||
display.getHoverPanelTrigger() == CardsDisplaySettings::HoverPanelTriggerShortcut);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::ensureZoomed(CardItem *card)
|
||||
{
|
||||
if (hoveredCard != card) {
|
||||
if (hoveredCard) {
|
||||
endCardHover(hoveredCard);
|
||||
}
|
||||
beginCardHover(card);
|
||||
hoveredCard = card;
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::fireTriggers(CardItem *card, bool zoomFired, bool panelFired)
|
||||
{
|
||||
if (!card || !card->getOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->getCardItem() == card) {
|
||||
cardHoverWidget->refreshDisplay();
|
||||
return;
|
||||
}
|
||||
|
||||
// Explicit triggers preempt any pending hover timing.
|
||||
hoverPopupTarget = nullptr;
|
||||
hoverPanelTarget = nullptr;
|
||||
hoverActivateTimer->stop();
|
||||
hoverPanelTimer->stop();
|
||||
|
||||
auto &display = SettingsCache::instance().cardsDisplay();
|
||||
const bool effectiveDouble =
|
||||
display.getHoverPanelDoubleConfirm() && display.getHoverZoomTrigger() == display.getHoverPanelTrigger();
|
||||
|
||||
if (zoomFired && panelFired) {
|
||||
// Both phases on one action: one-step zooms and shows at once (the crossfade
|
||||
// covers it); double-confirm arms on first fire and shows on repeat.
|
||||
if (effectiveDouble) {
|
||||
if (confirmArmedCard == card) {
|
||||
confirmArmedCard.clear();
|
||||
ensureZoomed(card);
|
||||
showCardHoverPopup(card);
|
||||
} else {
|
||||
confirmArmedCard = card;
|
||||
ensureZoomed(card);
|
||||
}
|
||||
} else {
|
||||
ensureZoomed(card);
|
||||
showCardHoverPopup(card);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (zoomFired) {
|
||||
ensureZoomed(card);
|
||||
}
|
||||
if (panelFired) {
|
||||
ensureZoomed(card);
|
||||
showCardHoverPopup(card);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::onHoverWidgetEntered()
|
||||
{
|
||||
hoverGracePending = false;
|
||||
hoverGraceTimer->stop();
|
||||
}
|
||||
|
||||
void GameScene::onHoverWidgetLeft()
|
||||
{
|
||||
if (!hoveredCard && cardHoverWidget && cardHoverWidget->isVisible() && !hoverGracePending) {
|
||||
hoverGracePending = true;
|
||||
hoverGraceTimer->start();
|
||||
}
|
||||
}
|
||||
|
||||
bool GameScene::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Resize) {
|
||||
positionCardHoverPopup();
|
||||
}
|
||||
return QGraphicsScene::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
CardZone *GameScene::findTopmostZone(const QList<QGraphicsItem *> &items)
|
||||
{
|
||||
for (QGraphicsItem *item : items) {
|
||||
|
|
@ -940,7 +1389,50 @@ bool GameScene::event(QEvent *event)
|
|||
{
|
||||
if (event->type() == QEvent::GraphicsSceneMouseMove) {
|
||||
updateHover(static_cast<QGraphicsSceneMouseEvent *>(event)->scenePos());
|
||||
} else if (event->type() == QEvent::GraphicsSceneWheel) {
|
||||
// Wheel trigger: wheeling over a card fires whichever phases listen to it
|
||||
// instead of scrolling. Fully consumed here: forwarding to the base dispatcher
|
||||
// as well would double-deliver it to items under the cursor (e.g. zone views,
|
||||
// which scroll on wheel). Scrolling up instead reverts an active zoom/panel —
|
||||
// backing out of the gesture undoes it; with nothing to revert the event falls
|
||||
// through to normal scrolling.
|
||||
auto &display = SettingsCache::instance().cardsDisplay();
|
||||
const bool zoomFired = display.getHoverZoomTrigger() == CardsDisplaySettings::HoverPanelTriggerWheel;
|
||||
const bool panelFired = display.getHoverPanelTrigger() == CardsDisplaySettings::HoverPanelTriggerWheel;
|
||||
if (zoomFired || panelFired) {
|
||||
auto *wheelEvent = static_cast<QGraphicsSceneWheelEvent *>(event);
|
||||
// QGraphicsSceneWheelEvent carries delta()/pixelDelta(), not angleDelta().
|
||||
const int wheelUp = !wheelEvent->pixelDelta().isNull()
|
||||
? wheelEvent->pixelDelta().y()
|
||||
: (wheelEvent->orientation() == Qt::Vertical ? wheelEvent->delta() : 0);
|
||||
if (wheelUp > 0) {
|
||||
if ((cardHoverWidget && cardHoverWidget->isVisible()) || hoveredCard) {
|
||||
hoverPopupTarget = nullptr;
|
||||
hoverPanelTarget = nullptr;
|
||||
confirmArmedCard.clear();
|
||||
hideCardHoverPopup();
|
||||
if (hoveredCard) {
|
||||
endCardHover(hoveredCard);
|
||||
hoveredCard = nullptr;
|
||||
}
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
} else if (CardItem *card = pickCardAt(wheelEvent->scenePos())) {
|
||||
fireTriggers(card, zoomFired, panelFired);
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
// The hover panel opens directly over the zoomed card under the cursor, which the
|
||||
// scene observes as a Leave. The cursor never actually left: if it is over the open
|
||||
// panel, keep the hover (dropping it here shrinks the card from under the panel and
|
||||
// starves the popup logic, so the menu never shows). A genuine leave of the window
|
||||
// misses the panel and still ends the hover as before.
|
||||
if (cardHoverWidget && cardHoverWidget->isVisible() && cardHoverWidget->isMouseOverWidget()) {
|
||||
return QGraphicsScene::event(event);
|
||||
}
|
||||
updateHoveredCard(nullptr);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaObject>
|
||||
#include <QPair>
|
||||
#include <QPointF>
|
||||
#include <QPointer>
|
||||
#include <QSize>
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
class CardZoneLogic;
|
||||
class IAnimatedItem;
|
||||
class QGraphicsItem;
|
||||
class QGraphicsObject;
|
||||
class QPoint;
|
||||
struct ArrowData;
|
||||
template <class T> class QSharedPointer;
|
||||
|
|
@ -48,6 +50,8 @@ class ServerInfo_Card;
|
|||
class PhasesToolbar;
|
||||
class QBasicTimer;
|
||||
class QObject;
|
||||
class QTimer;
|
||||
class CardHoverWidget;
|
||||
|
||||
/**
|
||||
* @class GameScene
|
||||
|
|
@ -75,14 +79,36 @@ private:
|
|||
QSize viewSize; ///< Current view size
|
||||
QPointer<CardItem> hoveredCard; ///< Currently hovered card
|
||||
QPointer<CardItem> activeCard; ///< Card the currently open context menu targets
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
/// Card armed for double-confirm panel summon (first trigger zooms, repeat shows).
|
||||
QPointer<CardItem> confirmArmedCard;
|
||||
/// Ancestor chain of the hovered card with pre-hover Z-values. Lifted while hovering
|
||||
/// because Qt orders subtrees level by level: the card z alone cannot beat a sibling
|
||||
/// zone, and the top ancestor alone cannot beat same-area sibling zones either — every
|
||||
/// level from the zone up must win its own comparison. Guarded pointers: if the card
|
||||
/// is destroyed mid-hover (or an ancestor goes away first), the guards null out and
|
||||
/// the next restore skips them instead of touching freed memory. An orphaned lift is
|
||||
/// invisible — areas and zones tile without overlapping and all UI layers still win —
|
||||
/// and any outstanding lift is restored by the next hover transition.
|
||||
QList<QPair<QPointer<QGraphicsObject>, qreal>> hoverLiftedAncestors;
|
||||
/// Card whose ancestor chain is currently lifted (cleared together with the chain).
|
||||
QPointer<CardItem> liftOwner;
|
||||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
QHash<QObject *, QMetaObject::Connection>
|
||||
animationItemConnections; ///< destroyed->removeAnimatedItem handles per animated item
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
|
||||
// Card hover popup
|
||||
QPointer<CardHoverWidget> cardHoverWidget; ///< The hover popup widget
|
||||
QTimer *hoverActivateTimer; ///< Dwell delay before the hover zoom kicks in
|
||||
QTimer *hoverPanelTimer; ///< Delay after the zoom before the tab panel shows
|
||||
QTimer *hoverGraceTimer; ///< Grace period before hiding popup (200ms)
|
||||
QPointer<CardItem> hoverPopupTarget; ///< Card the popup is intended for
|
||||
QPointer<CardItem> hoverPanelTarget; ///< Card the panel timer will show
|
||||
bool hoverGracePending = false; ///< Grace timer armed (mouse left a card or the popup)
|
||||
|
||||
/**
|
||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||
* @param scenePos Scene position of the cursor.
|
||||
|
|
@ -96,6 +122,15 @@ private:
|
|||
void beginCardHover(CardItem *card);
|
||||
/** @brief Deactivates hover state and restores the card to its clip container. */
|
||||
void endCardHover(CardItem *card);
|
||||
/** @brief Restores every lifted ancestor to its pre-hover Z-value and drops the chain. */
|
||||
void restoreHoverLift();
|
||||
/** @brief Settles a finished shrink-out: clip container and ancestor lift restore. */
|
||||
void onHoverShrinkFinished(AbstractCardItem *card);
|
||||
|
||||
// Card hover popup methods
|
||||
void showCardHoverPopup(CardItem *card);
|
||||
void hideCardHoverPopup();
|
||||
void positionCardHoverPopup();
|
||||
|
||||
public:
|
||||
/**
|
||||
|
|
@ -227,6 +262,21 @@ public:
|
|||
/** @brief Updates hovered card highlighting. */
|
||||
void updateHoveredCard(CardItem *newCard);
|
||||
|
||||
/** @brief Topmost card at a scene position, without side effects. */
|
||||
CardItem *pickCardAt(const QPointF &scenePos);
|
||||
|
||||
/**
|
||||
* @brief Fires the zoom and/or panel trigger for a card (one explicit user action).
|
||||
* When both fire on one action, double-confirm arms on first fire and shows on repeat.
|
||||
*/
|
||||
void fireTriggers(CardItem *card, bool zoomFired, bool panelFired);
|
||||
|
||||
/** @brief Starts the hover zoom for a card without touching panel state. */
|
||||
void ensureZoomed(CardItem *card);
|
||||
|
||||
/** @brief The currently hovered card, if any (used by zones to dodge it while painting). */
|
||||
CardItem *getHoveredCard() const;
|
||||
|
||||
/**
|
||||
* @brief Registers an item for animation updates with the shared scene timer.
|
||||
*
|
||||
|
|
@ -293,10 +343,22 @@ public slots:
|
|||
*/
|
||||
void onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner);
|
||||
|
||||
// Card hover popup slots
|
||||
void onHoverActivateTimeout();
|
||||
void onHoverPanelTimeout();
|
||||
void onHoverGraceTimeout();
|
||||
void onHoverWidgetEntered();
|
||||
void onHoverWidgetLeft();
|
||||
/** @brief Summons the hover panel for the currently hovered card, if any. */
|
||||
void summonHoverPanel();
|
||||
|
||||
protected:
|
||||
/** @brief Handles hover updates. */
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
/** @brief Repositions the hover popup when the viewport resizes. */
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
/** @brief Handles animation timer updates. */
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par
|
|||
|
||||
connect(aCloseMostRecentZoneView, &QAction::triggered, scene, &GameScene::closeMostRecentZoneView);
|
||||
addAction(aCloseMostRecentZoneView);
|
||||
|
||||
aShowHoverPanel = new QAction(this);
|
||||
connect(aShowHoverPanel, &QAction::triggered, scene, &GameScene::summonHoverPanel);
|
||||
addAction(aShowHoverPanel);
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&GameView::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
|
@ -246,6 +250,7 @@ void GameView::refreshShortcuts()
|
|||
{
|
||||
aCloseMostRecentZoneView->setShortcuts(
|
||||
SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView"));
|
||||
aShowHoverPanel->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("Player/aShowHoverPanel"));
|
||||
}
|
||||
|
||||
void GameView::clearTallyLabels()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class GameView : public QGraphicsView
|
|||
Q_OBJECT
|
||||
private:
|
||||
QAction *aCloseMostRecentZoneView;
|
||||
QAction *aShowHoverPanel;
|
||||
QRubberBand *rubberBand;
|
||||
QLabel *dragCountLabel;
|
||||
QLabel *totalCountLabel;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,15 @@ bool TableZone::isInverted() const
|
|||
|
||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
// The hovered (zoomed) card may spill over this zone: cut it out of every layer
|
||||
// below so washes, indicators, dimming and the land divider never paint over it,
|
||||
// whatever the stacking order is.
|
||||
const QRect hoverHole = hoveredCardHole();
|
||||
const bool hasHole = !hoverHole.isNull();
|
||||
if (hasHole) {
|
||||
painter->setClipRegion(QRegion(boundingRect().toRect()) - QRegion(hoverHole));
|
||||
}
|
||||
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish table zone from stack zone
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
|
||||
|
|
@ -142,6 +151,32 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
|
|||
}
|
||||
|
||||
paintLandDivider(painter);
|
||||
|
||||
if (hasHole) {
|
||||
painter->setClipping(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The hovered card's footprint in zone coordinates, or null when it does not
|
||||
* overlap this zone. Lets every paint layer dodge the zoomed card instead of painting
|
||||
* over it.
|
||||
*/
|
||||
QRect TableZone::hoveredCardHole() const
|
||||
{
|
||||
auto *scene = qobject_cast<GameScene *>(this->scene());
|
||||
if (!scene) {
|
||||
return {};
|
||||
}
|
||||
CardItem *hovered = scene->getHoveredCard();
|
||||
if (!hovered) {
|
||||
return {};
|
||||
}
|
||||
QRect hole = mapRectFromScene(hovered->sceneBoundingRect()).toRect();
|
||||
if (!hole.intersects(boundingRect().toRect())) {
|
||||
return {};
|
||||
}
|
||||
return hole & boundingRect().toRect();
|
||||
}
|
||||
|
||||
void TableZone::onPlaymatChanged(bool active)
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ private:
|
|||
|
||||
void paintZoneOutline(QPainter *painter);
|
||||
void paintLandDivider(QPainter *painter);
|
||||
[[nodiscard]] QRect hoveredCardHole() const;
|
||||
|
||||
/*
|
||||
Calculates card stack widths so mapping functions work properly
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue