diff --git a/src/SeerParallelStacksGraphicsView.cpp b/src/SeerParallelStacksGraphicsView.cpp index 7f556ed..a4c3a44 100644 --- a/src/SeerParallelStacksGraphicsView.cpp +++ b/src/SeerParallelStacksGraphicsView.cpp @@ -87,6 +87,12 @@ SeerParallelStacksStackBoxItem::SeerParallelStacksStackBoxItem(const SeerParalle setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); setAcceptHoverEvents(true); + _hoverTimer = new QTimer(this); + _hoverTimer->setSingleShot(true); + _hoverTimer->setInterval(_kHoverDelayMs); + + QObject::connect(_hoverTimer, &QTimer::timeout, this, &SeerParallelStacksStackBoxItem::handleShowPopup); + _headerLeft = QString("%1 Thread%2") .arg(stack.threadCount) .arg(stack.threadCount == 1 ? "" : "s"); _threadIds.resize(0); @@ -118,15 +124,15 @@ SeerParallelStacksStackBoxItem::SeerParallelStacksStackBoxItem(const SeerParalle QFont normFont; QFontMetrics normFm(normFont); - qreal headerW = boldFm.horizontalAdvance(_headerLeft) + boldFm.horizontalAdvance(_headerRight) + kHeaderGap; + qreal headerW = boldFm.horizontalAdvance(_headerLeft) + boldFm.horizontalAdvance(_headerRight) + _kHeaderGap; qreal maxTextW = headerW; for (const auto& frame : _stack.frames) { maxTextW = std::max(maxTextW, (qreal)normFm.horizontalAdvance(frame.function())); } - _width = maxTextW + 2 * kPadX; - _height = kPadY + kRowH * (1 + (int)_stack.frames.size()) + kPadY; + _width = maxTextW + 2 * _kPadX; + _height = _kPadY + _kRowH * (1 + (int)_stack.frames.size()) + _kPadY; qDebug() << _stack.stacks.size(); } @@ -163,19 +169,19 @@ void SeerParallelStacksStackBoxItem::paint(QPainter* painter, const QStyleOption QFont boldFont; boldFont.setBold(true); QFont normFont; - const qreal innerW = _width - 2 * kPadX; - qreal y = kPadY; + const qreal innerW = _width - 2 * _kPadX; + qreal y = _kPadY; painter->setFont(boldFont); painter->setPen(colors.headerText); - painter->drawText(QRectF(kPadX, y, innerW, kRowH), Qt::AlignLeft | Qt::AlignVCenter, _headerLeft); + painter->drawText(QRectF(_kPadX, y, innerW, _kRowH), Qt::AlignLeft | Qt::AlignVCenter, _headerLeft); if (!_headerRight.isEmpty()) { painter->setPen(colors.threadIdsText); - painter->drawText(QRectF(kPadX, y, innerW, kRowH), Qt::AlignRight | Qt::AlignVCenter, _headerRight); + painter->drawText(QRectF(_kPadX, y, innerW, _kRowH), Qt::AlignRight | Qt::AlignVCenter, _headerRight); } - y += kRowH; + y += _kRowH; painter->setPen(QPen(colors.divider, 1)); painter->drawLine(QPointF(0, y), QPointF(_width, y)); @@ -184,8 +190,8 @@ void SeerParallelStacksStackBoxItem::paint(QPainter* painter, const QStyleOption painter->setPen(colors.frameText); for (const auto& frame : _stack.frames) { - painter->drawText(QRectF(kPadX, y, innerW, kRowH), Qt::AlignLeft | Qt::AlignVCenter, frame.function()); - y += kRowH; + painter->drawText(QRectF(_kPadX, y, innerW, _kRowH), Qt::AlignLeft | Qt::AlignVCenter, frame.function()); + y += _kRowH; } if (_dragging) { @@ -269,37 +275,82 @@ void SeerParallelStacksStackBoxItem::hoverEnterEvent(QGraphicsSceneHoverEvent* e QGraphicsItem::hoverEnterEvent(event); if (_popup == nullptr) { - - QString frame; - - if (_stack.frames.size() > 0) { - frame = _stack.frames[0].function(); - } - - _popup = new SeerParallelStacksPopupTableWidget(); - - for (const auto& id : _threadIds) { - _popup->addRow(id, frame); - } - - QGraphicsView* view = scene()->views().first(); // scene() is always valid here - - // Position below the item, aligned to its left edge - QRectF sceneRect = mapToScene(boundingRect()).boundingRect(); - QPoint viewportPt = view->mapFromScene(sceneRect.bottomLeft()); - QPoint globalPt = view->viewport()->mapToGlobal(viewportPt); - _popup->move(globalPt); - - _popup->show(); - - // Connect things. - QObject::connect(_popup, &SeerParallelStacksPopupTableWidget::mouseLeftPopup, this, &SeerParallelStacksStackBoxItem::handleDeletePopup); + _hoverTimer->start(); } } void SeerParallelStacksStackBoxItem::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) { QGraphicsItem::hoverLeaveEvent(event); + + // Cancel a pending popup if the mouse left before the delay elapsed. + _hoverTimer->stop(); + + // A popup that's already showing stays up if the mouse just moved onto + // it — give handleMaybeClosePopup() a moment to check for that. + handleMaybeClosePopup(); +} + +QRect SeerParallelStacksStackBoxItem::globalRect() const { + + QGraphicsView* view = scene()->views().first(); // scene() is always valid here + + QRectF sceneRect = mapToScene(boundingRect()).boundingRect(); + QPoint topLeft = view->viewport()->mapToGlobal(view->mapFromScene(sceneRect.topLeft())); + QPoint bottomRight = view->viewport()->mapToGlobal(view->mapFromScene(sceneRect.bottomRight())); + + return QRect(topLeft, bottomRight); +} + +void SeerParallelStacksStackBoxItem::handleShowPopup() { + + if (_popup != nullptr) { + return; + } + + QString frame; + + if (_stack.frames.size() > 0) { + frame = _stack.frames[0].function(); + } + + _popup = new SeerParallelStacksPopupTableWidget(); + + for (const auto& id : _threadIds) { + _popup->addRow(id, frame); + } + + // Position below the item, aligned to its left edge + _popup->move(globalRect().bottomLeft()); + + _popup->show(); + + QObject::connect(_popup, &SeerParallelStacksPopupTableWidget::mouseLeftPopup, this, &SeerParallelStacksStackBoxItem::handleMaybeClosePopup); +} + +void SeerParallelStacksStackBoxItem::handleMaybeClosePopup() { + + if (_popup == nullptr) { + return; + } + + // The mouse may have simply crossed from the node onto the popup (or + // vice versa) — give that a moment to register before deciding the + // mouse has truly left both, then re-check with the cursor's live + // position rather than relying on possibly-stale hover state. + QTimer::singleShot(_kCloseGraceMs, this, [this]() { + + if (_popup == nullptr) { + return; + } + + bool overNode = globalRect().contains(QCursor::pos()); + bool overPopup = _popup->frameGeometry().contains(QCursor::pos()); + + if (overNode == false && overPopup == false) { + handleDeletePopup(); + } + }); } void SeerParallelStacksStackBoxItem::handleDeletePopup() { @@ -358,7 +409,7 @@ QRectF SeerParallelStacksLiveEdge::boundingRect() const { QPointF f = _from->sceneBottom(); QPointF t = _to->sceneTop(); - qreal pad = kArrow + kVCtrl + 4; + qreal pad = _kArrow + _kVCtrl + 4; return QRectF(f, t).normalized().adjusted(-pad, -pad, pad, pad); } @@ -382,39 +433,8 @@ void SeerParallelStacksLiveEdge::paint(QPainter* painter, const QStyleOptionGrap QPointF from = _from->sceneBottom(); QPointF to = _to->sceneTop(); - /* - // Bezier: control points pull vertically toward each other - QPainterPath path; - path.moveTo(from); - path.cubicTo(from + QPointF(0, kVCtrl), to + QPointF(0, -kVCtrl), to); - - painter->setPen(QPen(QColor(0x55, 0x55, 0x55), 1.5)); - painter->setBrush(Qt::NoBrush); - painter->drawPath(path); - - // Arrowhead at `to` pointing downward (into the parent box) - // The tangent direction at the end of the cubic is (to - cp2) - QPointF cp2 = to + QPointF(0, -kVCtrl); - QPointF dir = to - cp2; - double len = std::hypot(dir.x(), dir.y()); - if (len < 1e-6) return; - dir /= len; // normalise - - // Perpendicular - QPointF perp(-dir.y(), dir.x()); - - QPointF a1 = to - dir * kArrow + perp * (kArrow * 0.5); - QPointF a2 = to - dir * kArrow - perp * (kArrow * 0.5); - - QPolygonF arrowHead; - arrowHead << to << a1 << a2; - painter->setPen(Qt::NoPen); - painter->setBrush(QColor(0x55, 0x55, 0x55)); - painter->drawPolygon(arrowHead); - */ - // --- Arrowhead: tip touches the child box bottom (`from`), - // base is kArrow further along toward `to`. + // base is _kArrow further along toward `to`. // The bezier starts at the base so the line never overlaps the head. QPointF delta = to - from; double len = std::hypot(delta.x(), delta.y()); @@ -425,16 +445,16 @@ void SeerParallelStacksLiveEdge::paint(QPainter* painter, const QStyleOptionGrap QPointF perp(-dir.y(), dir.x()); // perpendicular QPointF tip = from; // touches child box - QPointF base = from + dir * kArrow; // where the line begins + QPointF base = from + dir * _kArrow; // where the line begins - QPointF a1 = base + perp * (kArrow * 0.5); - QPointF a2 = base - perp * (kArrow * 0.5); + QPointF a1 = base + perp * (_kArrow * 0.5); + QPointF a2 = base - perp * (_kArrow * 0.5); // Bezier runs from the arrowhead base to the parent box top. QPainterPath path; path.moveTo(base); - path.cubicTo(base + QPointF(0, kVCtrl), to + QPointF(0, -kVCtrl), to); + path.cubicTo(base + QPointF(0, _kVCtrl), to + QPointF(0, -_kVCtrl), to); const QColor color = edgeColor(); @@ -641,15 +661,6 @@ SeerParallelStacksPopupTableWidget::SeerParallelStacksPopupTableWidget(QWidget* QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(6, 6, 6, 6); layout->addWidget(_table); - - _closeTimer = new QTimer(this); - _closeTimer->setSingleShot(true); // Ensures it only fires once - _closeTimer->setInterval(1500); // Delay - - // Connect things. - QObject::connect(_closeTimer, &QTimer::timeout, this, &SeerParallelStacksPopupTableWidget::handleCloseTimer); - - _closeTimer->start(); } void SeerParallelStacksPopupTableWidget::addRow (int threadid, const QString& frame) { @@ -673,31 +684,13 @@ void SeerParallelStacksPopupTableWidget::addRow (int threadid, const QString& fr _table->resizeColumnToContents(1); } -void SeerParallelStacksPopupTableWidget::enterEvent(QEnterEvent* event) { - - // If the user moves the cursor into the table, cancel the timer. - if (_closeTimer->isActive()) { - _closeTimer->stop(); - } - - QFrame::enterEvent(event); -} - void SeerParallelStacksPopupTableWidget::leaveEvent(QEvent* event) { - // If the user leaves the table, ask for the table to be closed. QFrame::leaveEvent(event); emit mouseLeftPopup(); } -void SeerParallelStacksPopupTableWidget::handleCloseTimer() { - - // If the user doesn't move the cursor into the table in the default time, - // ask the table to be closed. - emit mouseLeftPopup(); -} - // ================================================================ // SeerParallelStacksPopupTableWidget // ================================================================ diff --git a/src/SeerParallelStacksGraphicsView.h b/src/SeerParallelStacksGraphicsView.h index d357853..901595c 100644 --- a/src/SeerParallelStacksGraphicsView.h +++ b/src/SeerParallelStacksGraphicsView.h @@ -52,26 +52,32 @@ class SeerParallelStacksStackBoxItem : public QObject, public QGraphicsItem { void hoverLeaveEvent (QGraphicsSceneHoverEvent* event) override; private slots: - void handleDeletePopup (); + void handleDeletePopup (); + void handleShowPopup (); + void handleMaybeClosePopup (); private: + // Global (screen) rect this box occupies, used to test whether the + // cursor is still over the node. + QRect globalRect () const; + + QVector _edges; // non-owning QVector _threadIds; SeerParallelStacksStack _stack; QString _headerLeft; QString _headerRight; - qreal _width = 0; - qreal _height = 0; - SeerParallelStacksPopupTableWidget* _popup = 0; - - bool _dragging = false; + qreal _width = 0; + qreal _height = 0; + SeerParallelStacksPopupTableWidget* _popup = 0; + QTimer* _hoverTimer = 0; + static constexpr int _kHoverDelayMs = 1000; + static constexpr int _kCloseGraceMs = 150; + bool _dragging = false; QPointF _dragOffset; - - QVector _edges; // non-owning - - static constexpr qreal kPadX = 12; - static constexpr qreal kPadY = 8; - static constexpr qreal kRowH = 20; - static constexpr qreal kHeaderGap = 16; + static constexpr qreal _kPadX = 12; + static constexpr qreal _kPadY = 8; + static constexpr qreal _kRowH = 20; + static constexpr qreal _kHeaderGap = 16; }; // --------------------------------------------------------------- @@ -82,7 +88,7 @@ class SeerParallelStacksLiveEdge : public QGraphicsItem { public: SeerParallelStacksLiveEdge(SeerParallelStacksStackBoxItem* from, SeerParallelStacksStackBoxItem* to, QGraphicsItem* parent = nullptr); - ~SeerParallelStacksLiveEdge() override; + ~SeerParallelStacksLiveEdge() override; QRectF boundingRect () const override; QPainterPath shape () const override; @@ -95,11 +101,11 @@ class SeerParallelStacksLiveEdge : public QGraphicsItem { private: - SeerParallelStacksStackBoxItem* _from; // child (bottom anchor) - SeerParallelStacksStackBoxItem* _to; // parent (top anchor) + SeerParallelStacksStackBoxItem* _from; // child (bottom anchor) + SeerParallelStacksStackBoxItem* _to; // parent (top anchor) - static constexpr qreal kArrow = 8.0; - static constexpr qreal kVCtrl = 60.0 * 0.4; // bezier control-point stretch + static constexpr qreal _kArrow = 8.0; + static constexpr qreal _kVCtrl = 60.0 * 0.4; // bezier control-point stretch }; @@ -142,7 +148,8 @@ class SeerParallelStacksMiniMapWidget : public QWidget { }; // A frameless popup window that wraps a QTableWidget inside a small -// bordered frame, and closes/deletes itself when the mouse leaves it. +// bordered frame. Its lifetime is owned by the SeerParallelStacksStackBoxItem +// that created it — it stays up for as long as the mouse is in that node. class SeerParallelStacksPopupTableWidget : public QFrame { Q_OBJECT @@ -153,18 +160,13 @@ class SeerParallelStacksPopupTableWidget : public QFrame { void addRow (int threadid, const QString& function); protected: - void enterEvent (QEnterEvent* event) override; void leaveEvent (QEvent* event) override; - protected slots: - void handleCloseTimer (); - signals: void mouseLeftPopup (); private: QTableWidget* _table; - QTimer* _closeTimer; }; class SeerParallelStacksGraphicsView : public QGraphicsView {