diff --git a/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp b/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp index 742d3703a..b7d236b15 100644 --- a/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_hover_widget.cpp @@ -9,6 +9,7 @@ #include "card_info_picture_widget.h" #include "card_info_text_widget.h" +#include #include #include #include @@ -53,6 +54,20 @@ static constexpr int REFRESH_DEBOUNCE_MS = 30; static constexpr int OVERLAY_BTN_SIZE = 24; static constexpr int OVERLAY_CORNER_RADIUS = 4; +// P/T pill: centred on the card's printed P/T box, in fractions of the picture rect. +static constexpr double PT_PILL_ANCHOR_X = 0.92; +static constexpr double PT_PILL_ANCHOR_Y = 0.94; +static constexpr int PT_PILL_EDGE_INSET = 2; +static constexpr int PT_PILL_MIN_WIDTH = 48; +static constexpr int PT_PILL_MIN_HEIGHT = 22; +static constexpr int PT_PILL_TEXT_PADDING = 4; + +// Hover trigger: the bottom-right quadrant of the art, around the printed P/T box. +static constexpr double PT_HOVER_REGION_WIDTH = 0.35; +static constexpr double PT_HOVER_REGION_HEIGHT = 0.20; +// Halo around the resting pill that still counts as "on the P/T box" for opening the rows. +static constexpr int PT_OPEN_HALO = 4; + enum HoverPanelPage { PAGE_INFO, @@ -181,6 +196,11 @@ void CardHoverWidget::applyThemeStyle() " color: %4; background: %3; border: 1px solid %2;" " border-radius: 3px; padding: 2px;" " font-weight: bold; font-size: 13px;" + "}" + // Accent edge on the resting P/T pill: the cue that the + // control rows live behind it, and the divider once they lift out. + "QLabel#cardHoverPTPill {" + " border-top: 2px solid %5;" "}") .arg(window, mid, base, text, highlight, highlightedText, disabled, windowText); if (sheet == styleSheet()) { @@ -198,6 +218,15 @@ static QString overlayBgStyle() .arg(OVERLAY_CORNER_RADIUS); } +/** @brief Clamps @p start so that a run of @p size stays within [@p origin, @p origin + @p extent]. */ +static int clampToSpan(int origin, int extent, int size, int start) +{ + if (size >= extent) { + return origin; + } + return qBound(origin, start, origin + extent - size); +} + // ---------- Image area with overlaid controls ---------- void CardHoverWidget::createImageArea() @@ -223,6 +252,10 @@ void CardHoverWidget::createImageArea() createPTOverlay(); pic->installEventFilter(this); + // The artwork rect is only known after the pixmap is drawn (letterboxing, sideways-card + // rotation), so re-anchor the overlays once it lands. + connect(pic, &CardInfoPictureWidget::artRectChanged, this, &CardHoverWidget::positionOverlays, + Qt::QueuedConnection); } // ---------- Name bar overlay (top of card) ---------- @@ -282,7 +315,7 @@ void CardHoverWidget::createNameBarOverlay() nameLabel->installEventFilter(this); } -// ---------- P/T overlay (bottom-right of card) ---------- +// ---------- P/T overlay (bottom-right of card): pill at rest, controls on hover ---------- void CardHoverWidget::createPTOverlay() { @@ -302,14 +335,35 @@ void CardHoverWidget::createPTOverlay() : QList(); }; - ptPowerLabel = new QLabel(ptOverlay); + // Control rows: hidden at rest, shown while the P/T area is hovered. + ptControls = new QWidget(ptOverlay); + ptControls->setObjectName("cardHoverPTControls"); + ptControls->setMouseTracking(true); + ptControls->setStyleSheet(QStringLiteral("background: transparent;")); + + // Value pill: the resting state, and the bottom (never-moving) row of the stack. + // The accent top edge is the only cue that controls sit behind the pill; it doubles + // as the divider between the pill and the rows once they are lifted out. + ptPillLabel = new QLabel(ptOverlay); + ptPillLabel->setObjectName("cardHoverPTPill"); + ptPillLabel->setAlignment(Qt::AlignCenter); + // The accent top edge (the only cue that controls sit behind the pill) comes from the + // palette-driven sheet in applyThemeStyle(), so it follows theme switches. + ptPillLabel->setStyleSheet(QStringLiteral("color: white; font-weight: bold; font-size: 12px;" + " background: transparent; padding: 0 %1px;") + .arg(PT_PILL_TEXT_PADDING)); + ptPillLabel->setMinimumHeight(PT_PILL_MIN_HEIGHT); + // Seed text: the resting size has to exist before the first card is shown. + ptPillLabel->setText(QStringLiteral("00/00")); + + ptPowerLabel = new QLabel(ptControls); ptPowerLabel->setObjectName("cardHoverPTPowerValue"); ptPowerLabel->setAlignment(Qt::AlignCenter); ptPowerLabel->setStyleSheet( "color: white; font-weight: bold; font-size: 12px; background: transparent; padding: 0 2px;"); ptPowerLabel->setMinimumWidth(28); - ptToughLabel = new QLabel(ptOverlay); + ptToughLabel = new QLabel(ptControls); ptToughLabel->setObjectName("cardHoverPTToughValue"); ptToughLabel->setAlignment(Qt::AlignCenter); ptToughLabel->setStyleSheet( @@ -364,13 +418,37 @@ void CardHoverWidget::createPTOverlay() btnRow->addWidget(btnSetPT); btnRow->addWidget(btnResetPT); + auto *controlsLayout = new QVBoxLayout(ptControls); + controlsLayout->setContentsMargins(4, 3, 4, 3); + controlsLayout->setSpacing(2); + controlsLayout->addLayout(powerRow); + controlsLayout->addLayout(toughRow); + controlsLayout->addLayout(btnRow); + ptControls->setLayout(controlsLayout); + auto *ptLayout = new QVBoxLayout(ptOverlay); - ptLayout->setContentsMargins(4, 3, 4, 3); + ptLayout->setContentsMargins(0, 0, 0, 0); ptLayout->setSpacing(2); - ptLayout->addLayout(powerRow); - ptLayout->addLayout(toughRow); - ptLayout->addLayout(btnRow); + ptLayout->addWidget(ptControls); + ptLayout->addWidget(ptPillLabel); ptOverlay->setLayout(ptLayout); + + // Both states are driven from sizes cached here: hiding the controls would shrink + // layout()->sizeHint(), so the two states must not be measured from the live layout. + ptFullSize = ptLayout->sizeHint(); + ptControls->setVisible(false); + ptCompactSize = measureCompactPTSize(); + ptPillLabel->clear(); + ptOverlay->resize(ptCompactSize); + + // The whole subtree feeds the hover state: moving onto a button must not read as a leave. + ptOverlay->installEventFilter(this); + const QList ptChildren = ptOverlay->findChildren(); + for (QWidget *child : ptChildren) { + child->installEventFilter(this); + } + + ptOverlay->setVisible(false); } // ---------- Nav strip: vertical icon buttons with horizontal text ---------- @@ -1072,7 +1150,6 @@ void CardHoverWidget::positionOverlays() nameBarOverlay->show(); int containerWidth = imageContainer->width(); - int containerHeight = imageContainer->height(); // Name bar at top — width constrained, height from layout int nameWidth = containerWidth - 8; @@ -1080,23 +1157,153 @@ void CardHoverWidget::positionOverlays() nameBarOverlay->adjustSize(); nameBarOverlay->move(4, 4); - // P/T overlay at bottom-right - ptOverlay->adjustSize(); - int ptWidth = ptOverlay->sizeHint().width(); - int ptHeight = ptOverlay->sizeHint().height(); - ptOverlay->setGeometry(containerWidth - ptWidth - 4, containerHeight - ptHeight - 4, ptWidth, ptHeight); - - // Show/hide based on card data - bool hasPT = !currentCard->getPT().isEmpty(); + // Show/hide based on card data; an empty P/T always falls back to the compact pill + const bool hasPT = !currentCard->getPT().isEmpty(); + if (!hasPT && ptExpanded) { + setPTExpanded(false); + } + applyPTOverlayGeometry(); ptOverlay->setVisible(hasPT); } +void CardHoverWidget::setPTExpanded(bool expanded) +{ + if (!ptOverlay || !ptControls) { + return; + } + // Nothing to expand into when the card has no P/T. + if (expanded && (!currentCard || currentCard->getPT().isEmpty())) { + expanded = false; + } + // Mouse movement drives this on every event; only apply a real state change. + if (ptExpanded == expanded) { + return; + } + + ptExpanded = expanded; + ptControls->setVisible(expanded); + if (expanded) { + // The stack covers more of the art than the pill did; keep it above sibling overlays. + ptOverlay->raise(); + } + applyPTOverlayGeometry(); +} + +void CardHoverWidget::applyPTOverlayGeometry() +{ + if (!ptOverlay || !pic || ptCompactSize.isEmpty() || ptFullSize.isEmpty()) { + return; + } + // Anchor on the drawn artwork, not on imageContainer: the container can be taller than the + // picture, and sideways-layout cards are letterboxed inside pic. Both used to drop the + // overlay below the printed P/T box. + const QRect picture = + pic->artRect() + .translated(pic->pos()) + .adjusted(PT_PILL_EDGE_INSET, PT_PILL_EDGE_INSET, -PT_PILL_EDGE_INSET, -PT_PILL_EDGE_INSET); + const QRect compact = compactPillRect(); + if (picture.isEmpty() || compact.isEmpty()) { + return; + } + + // Expanded: same bottom edge and right edge as the pill, so the controls grow upward + // and never appear under the cursor that is resting on the pill. Never narrower than + // the current pill, or a three-digit value would clip against the stack's edge. + const QSize size = + ptExpanded ? QSize(qMax(ptFullSize.width(), ptCompactSize.width()), ptFullSize.height()) : ptCompactSize; + const int x = clampToSpan(picture.x(), picture.width(), size.width(), compact.x() + compact.width() - size.width()); + const int y = + clampToSpan(picture.y(), picture.height(), size.height(), compact.y() + compact.height() - size.height()); + + ptOverlay->setGeometry(x, y, size.width(), size.height()); +} + +QRect CardHoverWidget::compactPillRect() const +{ + if (!pic || ptCompactSize.isEmpty()) { + return {}; + } + const QRect picture = + pic->artRect() + .translated(pic->pos()) + .adjusted(PT_PILL_EDGE_INSET, PT_PILL_EDGE_INSET, -PT_PILL_EDGE_INSET, -PT_PILL_EDGE_INSET); + if (picture.isEmpty()) { + return {}; + } + // Centred on the printed P/T box (bottom-right of the art). + const int anchorX = picture.x() + qRound(picture.width() * PT_PILL_ANCHOR_X); + const int anchorY = picture.y() + qRound(picture.height() * PT_PILL_ANCHOR_Y); + return QRect( + clampToSpan(picture.x(), picture.width(), ptCompactSize.width(), anchorX - ptCompactSize.width() / 2), + clampToSpan(picture.y(), picture.height(), ptCompactSize.height(), anchorY - ptCompactSize.height() / 2), + ptCompactSize.width(), ptCompactSize.height()); +} + +QRect CardHoverWidget::ptOpenRegion() const +{ + const QRect pill = compactPillRect(); + if (pill.isEmpty() || !pic) { + return {}; + } + // The opening target is the P/T box itself (plus a small halo), not the whole bottom-right + // quadrant: a wider trigger lifts the rows out from under a cursor that merely passed by, + // and the click that selects the card would land on a P/T button instead. + return pill.translated(-pic->pos()).adjusted(-PT_OPEN_HALO, -PT_OPEN_HALO, PT_OPEN_HALO, PT_OPEN_HALO); +} + +QRect CardHoverWidget::ptHoverRegion() const +{ + if (!pic) { + return {}; + } + // The band is relative to the drawn artwork, so a letterboxed (sideways-layout) card keeps + // its P/T corner where the printed box actually is. + const QRect art = pic->artRect(); + if (art.isEmpty()) { + return {}; + } + return QRect(art.x() + qRound(art.width() * (1.0 - PT_HOVER_REGION_WIDTH)), + art.y() + qRound(art.height() * (1.0 - PT_HOVER_REGION_HEIGHT)), + qRound(art.width() * PT_HOVER_REGION_WIDTH), qRound(art.height() * PT_HOVER_REGION_HEIGHT)); +} + +bool CardHoverWidget::isCursorOverPTArea() const +{ + if (!ptOverlay || !pic || !imageContainer) { + return false; + } + if (ptOverlay->rect().contains(ptOverlay->mapFromGlobal(QCursor::pos()))) { + return true; + } + return ptHoverRegion().translated(pic->pos()).contains(imageContainer->mapFromGlobal(QCursor::pos())); +} + +QSize CardHoverWidget::measureCompactPTSize() const +{ + if (!ptPillLabel) { + return QSize(PT_PILL_MIN_WIDTH, PT_PILL_MIN_HEIGHT); + } + // QLabel::sizeHint() leaves out the stylesheet padding, so add it back explicitly. + const QSize hint = ptPillLabel->sizeHint(); + return QSize(qMax(PT_PILL_MIN_WIDTH, hint.width() + 2 * PT_PILL_TEXT_PADDING), + qMax(PT_PILL_MIN_HEIGHT, hint.height())); +} + void CardHoverWidget::resizeEvent(QResizeEvent *event) { QFrame::resizeEvent(event); positionOverlays(); } +void CardHoverWidget::hideEvent(QHideEvent *event) +{ + // A reopened panel for the same card must not come back with the rows still lifted. + setPTExpanded(false); + // The window is already hidden here; repaint whatever it was covering. + invalidateCoveredArea(); + QFrame::hideEvent(event); +} + // ---------- Display updates ---------- void CardHoverWidget::updatePTDisplay() @@ -1104,6 +1311,8 @@ void CardHoverWidget::updatePTDisplay() if (!currentCard) { ptPowerLabel->clear(); ptToughLabel->clear(); + ptPillLabel->clear(); + setPTExpanded(false); return; } @@ -1111,6 +1320,8 @@ void CardHoverWidget::updatePTDisplay() if (pt.isEmpty()) { ptPowerLabel->setText("--"); ptToughLabel->setText("--"); + ptPillLabel->setText("--"); + setPTExpanded(false); } else { int slashIdx = pt.indexOf('/'); if (slashIdx >= 0) { @@ -1120,7 +1331,14 @@ void CardHoverWidget::updatePTDisplay() ptPowerLabel->setText(pt); ptToughLabel->setText("--"); } + ptPillLabel->setText(pt); } + + // Three-digit values (100/100, -1/-1) are legal, so the resting pill cannot keep a + // size measured from the seed text: re-measure on every value change and carry the + // width into the expanded stack. + ptCompactSize = measureCompactPTSize(); + applyPTOverlayGeometry(); } // ---------- Card data ---------- @@ -1148,6 +1366,8 @@ void CardHoverWidget::setCard(CardItem *card, PlayerGraphicsItem *player) connectCardSignals(); refreshDisplay(); rebuildActionsForZone(); + // A new card always starts compact; the rows only lift out while P/T is hovered. + setPTExpanded(false); positionOverlays(); // The widget may have been disabled by a previous teardown; it is interactive again. setEnabled(true); @@ -1192,6 +1412,8 @@ void CardHoverWidget::clearCard() annotationLabel->setVisible(false); ptPowerLabel->clear(); ptToughLabel->clear(); + ptPillLabel->clear(); + setPTExpanded(false); ptOverlay->setVisible(false); clearLayout(mainPageLayout); clearLayout(movePageLayout); @@ -1261,6 +1483,30 @@ void CardHoverWidget::positionNextTo(const QRectF &cardSceneRect, move(x, y); resize(popupSize); + invalidateCoveredArea(); +} + +void CardHoverWidget::invalidateCoveredArea() +{ + // The panel is an overlay window: moving or hiding it does not reliably invalidate + // the pixels it uncovered in the game view, which then stay stale (a black square) + // until something else repaints that area. Do it explicitly, unioned with the + // previous placement so a move repaints both sides. + QWidget *viewport = parentWidget(); + if (!viewport) { + lastViewportRect = QRect(); + return; + } + + QRect covered; + if (isVisible()) { + covered = QRect(viewport->mapFromGlobal(geometry().topLeft()), size()); + } + const QRect dirty = lastViewportRect.united(covered); + if (!dirty.isEmpty()) { + viewport->update(dirty.adjusted(-1, -1, 1, 1)); + } + lastViewportRect = covered; } // ---------- Show / hide: no window travel, the board zoom is the morph ---------- @@ -1287,16 +1533,27 @@ void CardHoverWidget::showAnimated() fadeIn->setStartValue(windowOpacity()); fadeIn->setEndValue(1.0); fadeIn->start(); + invalidateCoveredArea(); } void CardHoverWidget::hideAnimated() { + if (isClosing()) { + // Repeated hide requests must not restart the fade, or the panel never finishes + // closing while the cursor keeps moving. + return; + } fadeIn->stop(); fadeOut->setStartValue(windowOpacity()); fadeOut->setEndValue(0.0); fadeOut->start(); } +bool CardHoverWidget::isClosing() const +{ + return fadeOut->state() == QAbstractAnimation::Running; +} + // ---------- Event filter ---------- bool CardHoverWidget::eventFilter(QObject *obj, QEvent *event) @@ -1323,6 +1580,38 @@ bool CardHoverWidget::eventFilter(QObject *obj, QEvent *event) } } + // The P/T trigger is the printed P/T area, not just the pill: hovering that quadrant of + // the art lifts the control rows out. Driving it from pic's mouse moves (instead of an + // invisible child widget over the picture) keeps click-to-select working underneath. + if (obj == pic) { + if (event->type() == QEvent::MouseMove) { + const auto *me = static_cast(event); + const QPoint pos = me->position().toPoint(); + if (ptOpenRegion().contains(pos)) { + setPTExpanded(true); + } else if (!isCursorOverPTArea()) { + setPTExpanded(false); + } + } else if (event->type() == QEvent::Leave && !isCursorOverPTArea()) { + setPTExpanded(false); + } + } + + // P/T overlay: hovering the P/T area lifts the control rows out of the pill. The + // filter sits on the whole subtree so moving onto a button keeps it expanded. + const auto *ptWidget = qobject_cast(obj); + if (ptWidget && ptOverlay && (ptWidget == ptOverlay || ptOverlay->isAncestorOf(ptWidget))) { + if (event->type() == QEvent::Enter) { + setPTExpanded(true); + } else if (event->type() == QEvent::Leave) { + // Moving onto a child widget fires Leave on the parent too; only collapse once + // the cursor has left both the overlay and the printed P/T area. + if (!isCursorOverPTArea()) { + setPTExpanded(false); + } + } + } + return QFrame::eventFilter(obj, event); } diff --git a/cockatrice/src/interface/widgets/cards/card_hover_widget.h b/cockatrice/src/interface/widgets/cards/card_hover_widget.h index d76d6b29d..05c0d3e75 100644 --- a/cockatrice/src/interface/widgets/cards/card_hover_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_hover_widget.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ class QStackedWidget; class QLabel; class QLineEdit; class QEnterEvent; +class QHideEvent; class QPropertyAnimation; class QResizeEvent; class QTimer; @@ -56,6 +58,9 @@ public: void showAnimated(); void hideAnimated(); + /** @brief True while the panel is fading out towards a hide. */ + bool isClosing() const; + /** @brief Refreshes the PT, counter, and annotation labels from the current card. */ void refreshDisplay(); @@ -70,6 +75,7 @@ protected: void enterEvent(QEnterEvent *event) override; void leaveEvent(QEvent *event) override; void resizeEvent(QResizeEvent *event) override; + void hideEvent(QHideEvent *event) override; signals: void hoverEntered(); @@ -88,6 +94,10 @@ private: // Which side of the card the panel ended up on (drives the image-adjacent layout flip) bool popupOnRight = true; + // Viewport area the panel covered at its last placement; used to repaint what the + // overlay window uncovers when it moves or hides. + QRect lastViewportRect; + // Coalesces rapid state-change bursts into a single refresh QTimer *refreshTimer; @@ -102,8 +112,14 @@ private: QLineEdit *nameEdit; QLabel *annotationLabel; - // P/T controls overlay (bottom-right of card image) + // P/T overlay (bottom-right of card image): a compact value pill at rest that + // lifts its control rows out of itself while the P/T area is hovered. QWidget *ptOverlay; + QWidget *ptControls; + QLabel *ptPillLabel; + bool ptExpanded = false; + QSize ptCompactSize; + QSize ptFullSize; QLabel *ptPowerLabel; QLabel *ptToughLabel; QPushButton *btnIncP; @@ -165,6 +181,22 @@ private: void addRelatedViewButtons(QVBoxLayout *layout); void updatePTDisplay(); void positionOverlays(); + /** @brief Shows/hides the P/T control rows; the value pill stays at the bottom. */ + void setPTExpanded(bool expanded); + /** @brief Anchors the pill on the printed P/T box, or the expanded stack above it. */ + void applyPTOverlayGeometry(); + /** @brief Resting pill geometry, in @c imageContainer coordinates. */ + QRect compactPillRect() const; + /** @brief Pill plus halo: the only area that opens the rows, in @c pic coordinates. */ + QRect ptOpenRegion() const; + /** @brief Bottom-right band the rows stay open in (never opens them), in @c pic coordinates. */ + QRect ptHoverRegion() const; + /** @brief True while the cursor is over the overlay or the printed P/T area. */ + bool isCursorOverPTArea() const; + /** @brief Re-measures the resting pill from its current text. */ + QSize measureCompactPTSize() const; + /** @brief Repaints the viewport area this window covers and the one it just left. */ + void invalidateCoveredArea(); void clearLayout(QLayout *layout); diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp index 8f02ebcde..0d0c11fdf 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.cpp @@ -175,6 +175,8 @@ void CardInfoPictureWidget::setScaleFactor(const int scale) void CardInfoPictureWidget::updatePixmap() { pixmapDirty = true; + // The artwork rect belongs to the pixmap that is about to be loaded. + artRectCache = QRect(); update(); } @@ -252,6 +254,24 @@ void CardInfoPictureWidget::paintEvent(QPaintEvent *event) // Draw the pre-scaled pixmap directly painter.drawPixmap(targetRect, finalPixmap); + + if (targetRect != artRectCache) { + artRectCache = targetRect; + // Overlays anchored on the artwork (rather than the widget) re-position on this. + emit artRectChanged(); + } +} + +/** + * @brief Rect of the drawn artwork inside this widget. + * + * Mirrors the target rect paintEvent() computes: the pixmap scaled with KeepAspectRatio and + * centred, after the optional sideways-layout rotation. Falls back to rect() until the first + * paint, which is the right answer for the usual portrait card. + */ +QRect CardInfoPictureWidget::artRect() const +{ + return artRectCache.isEmpty() ? rect() : artRectCache; } /** diff --git a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h index 0eb731d92..5cd7e912a 100644 --- a/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/interface/widgets/cards/card_info_picture_widget.h @@ -41,6 +41,14 @@ public: return exactCard; } [[nodiscard]] QSize sizeHint() const override; + /** + * @brief Rect of the drawn artwork inside this widget. + * + * The pixmap is scaled with KeepAspectRatio and centred, and sideways-layout cards may be + * rotated, so this is usually smaller than rect(). Callers that overlay the picture (rather + * than the widget) must anchor on this. Falls back to rect() until the first paint. + */ + [[nodiscard]] QRect artRect() const; /** * @brief The size a card picture has at @p scaleFactor percent. @@ -60,6 +68,8 @@ signals: void cardScaleFactorChanged(int _scale); void cardChanged(const ExactCard &card); void cardClicked(QMouseEvent *event, const ExactCard &card); + /** @brief Emitted when the drawn artwork rect changes (size, scale factor, rotation, card). */ + void artRectChanged(); protected: void resizeEvent(QResizeEvent *event) override; @@ -83,6 +93,8 @@ private: double scaleFactor = 100; QPixmap resizedPixmap; bool pixmapDirty; + /// Last drawn artwork rect, in widget coordinates; empty until the first paint. + QRect artRectCache; bool hoverToZoomEnabled; bool raiseOnEnter;