From f981fe456d91c7685e696d9114f8c85d2467d108 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Thu, 5 Mar 2026 18:25:40 -0700 Subject: [PATCH] feat: see-through popup dismiss for disasm/value-history/struct-preview Override mouseMoveEvent in all three popup classes to forward mouse position back to viewport hover logic. When the row underneath the popup represents a different node, the popup dismisses automatically, allowing rapid swiping through FuncPtr rows. --- src/editor.cpp | 112 ++++++++++++++++++++++++++++++++++++--- src/editor.h | 2 + tests/test_editor.cpp | 119 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 6 deletions(-) diff --git a/src/editor.cpp b/src/editor.cpp index da8b3d0..db5e937 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -40,18 +40,30 @@ class ValueHistoryPopup : public QFrame { QStringList m_values; QVector m_labels; std::function m_onSet; + std::function m_onMouseMove; public: explicit ValueHistoryPopup(QWidget* parent) : QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_DeleteOnClose, false); setAttribute(Qt::WA_ShowWithoutActivating, true); + setMouseTracking(true); setFrameShape(QFrame::NoFrame); setAutoFillBackground(true); } uint64_t nodeId() const { return m_nodeId; } + bool hasButtons() const { return m_hasButtons; } void setOnSet(std::function fn) { m_onSet = std::move(fn); } + void setOnMouseMove(std::function fn) { m_onMouseMove = std::move(fn); } +protected: + void mouseMoveEvent(QMouseEvent* e) override { + if (!m_hasButtons && m_onMouseMove) + m_onMouseMove(e); + else + QFrame::mouseMoveEvent(e); + } +public: void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font, bool showButtons = false) { @@ -185,12 +197,14 @@ class DisasmPopup : public QFrame { QString m_body; QLabel* m_titleLabel = nullptr; QLabel* m_bodyLabel = nullptr; + std::function m_onMouseMove; public: explicit DisasmPopup(QWidget* parent) : QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_DeleteOnClose, false); setAttribute(Qt::WA_ShowWithoutActivating, true); + setMouseTracking(true); setFrameShape(QFrame::NoFrame); setAutoFillBackground(true); @@ -216,8 +230,14 @@ public: vbox->addWidget(m_bodyLabel); } + void setOnMouseMove(std::function fn) { m_onMouseMove = std::move(fn); } uint64_t nodeId() const { return m_nodeId; } - +protected: + void mouseMoveEvent(QMouseEvent* e) override { + if (m_onMouseMove) m_onMouseMove(e); + else QFrame::mouseMoveEvent(e); + } +public: void populate(uint64_t nodeId, const QString& title, const QString& body, const QFont& font) { if (nodeId == m_nodeId && body == m_body && isVisible()) @@ -283,12 +303,14 @@ class StructPreviewPopup : public QFrame { QString m_body; QLabel* m_titleLabel = nullptr; QLabel* m_bodyLabel = nullptr; + std::function m_onMouseMove; public: explicit StructPreviewPopup(QWidget* parent) : QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_DeleteOnClose, false); setAttribute(Qt::WA_ShowWithoutActivating, true); + setMouseTracking(true); setFrameShape(QFrame::NoFrame); setAutoFillBackground(true); @@ -315,7 +337,13 @@ public: } uint64_t nodeId() const { return m_nodeId; } - + void setOnMouseMove(std::function fn) { m_onMouseMove = std::move(fn); } +protected: + void mouseMoveEvent(QMouseEvent* e) override { + if (m_onMouseMove) m_onMouseMove(e); + else QFrame::mouseMoveEvent(e); + } +public: void populate(uint64_t nodeId, const QString& title, const QString& body, const QFont& font) { if (nodeId == m_nodeId && body == m_body && isVisible()) @@ -3002,8 +3030,26 @@ void RcxEditor::applyHoverCursor() { if (lm.heatLevel > 0 && lm.nodeId != 0) { auto it = m_valueHistory->find(lm.nodeId); if (it != m_valueHistory->end() && it->uniqueCount() > 1) { - if (!m_historyPopup) + if (!m_historyPopup) { m_historyPopup = new ValueHistoryPopup(this); + static_cast(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) { + QPoint gp = e->globalPosition().toPoint(); + QPoint vp = m_sci->viewport()->mapFromGlobal(gp); + m_lastHoverPos = vp; + m_hoverInside = m_sci->viewport()->rect().contains(vp); + if (!m_editState.active) { + auto h2 = hitTest(m_lastHoverPos); + uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0; + int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1; + if (nid != m_hoveredNodeId || nln != m_hoveredLine) { + m_hoveredNodeId = nid; + m_hoveredLine = nln; + applyHoverHighlight(); + } + } + applyHoverCursor(); + }); + } auto* popup = static_cast(m_historyPopup); popup->setOnSet([this](const QString& val) { if (!m_editState.active) return; @@ -3163,8 +3209,26 @@ void RcxEditor::applyHoverCursor() { QString lineText = getLineText(m_sci, h.line); ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW); if (vs.valid && h.col >= vs.start && h.col < vs.end) { - if (!m_historyPopup) + if (!m_historyPopup) { m_historyPopup = new ValueHistoryPopup(this); + static_cast(m_historyPopup)->setOnMouseMove([this](QMouseEvent* e) { + QPoint gp = e->globalPosition().toPoint(); + QPoint vp = m_sci->viewport()->mapFromGlobal(gp); + m_lastHoverPos = vp; + m_hoverInside = m_sci->viewport()->rect().contains(vp); + if (!m_editState.active) { + auto h2 = hitTest(m_lastHoverPos); + uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0; + int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1; + if (nid != m_hoveredNodeId || nln != m_hoveredLine) { + m_hoveredNodeId = nid; + m_hoveredLine = nln; + applyHoverHighlight(); + } + } + applyHoverCursor(); + }); + } auto* popup = static_cast(m_historyPopup); popup->populate(lm.nodeId, *it, editorFont(), false); long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, @@ -3248,8 +3312,26 @@ void RcxEditor::applyHoverCursor() { } } if (!body.isEmpty()) { - if (!m_disasmPopup) + if (!m_disasmPopup) { m_disasmPopup = new DisasmPopup(this); + static_cast(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) { + QPoint gp = e->globalPosition().toPoint(); + QPoint vp = m_sci->viewport()->mapFromGlobal(gp); + m_lastHoverPos = vp; + m_hoverInside = m_sci->viewport()->rect().contains(vp); + if (!m_editState.active) { + auto h2 = hitTest(m_lastHoverPos); + uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0; + int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1; + if (nid != m_hoveredNodeId || nln != m_hoveredLine) { + m_hoveredNodeId = nid; + m_hoveredLine = nln; + applyHoverHighlight(); + } + } + applyHoverCursor(); + }); + } auto* popup = static_cast( m_disasmPopup); popup->populate(lm.nodeId, title, body, @@ -3317,8 +3399,26 @@ void RcxEditor::applyHoverCursor() { } } if (!body.isEmpty()) { - if (!m_structPreviewPopup) + if (!m_structPreviewPopup) { m_structPreviewPopup = new StructPreviewPopup(this); + static_cast(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) { + QPoint gp = e->globalPosition().toPoint(); + QPoint vp = m_sci->viewport()->mapFromGlobal(gp); + m_lastHoverPos = vp; + m_hoverInside = m_sci->viewport()->rect().contains(vp); + if (!m_editState.active) { + auto h2 = hitTest(m_lastHoverPos); + uint64_t nid = (m_hoverInside && h2.line >= 0) ? h2.nodeId : 0; + int nln = (m_hoverInside && h2.line >= 0) ? h2.line : -1; + if (nid != m_hoveredNodeId || nln != m_hoveredLine) { + m_hoveredNodeId = nid; + m_hoveredLine = nln; + applyHoverHighlight(); + } + } + applyHoverCursor(); + }); + } auto* popup = static_cast(m_structPreviewPopup); popup->populate(lm.nodeId, lm.pointerTargetName, body, editorFont()); diff --git a/src/editor.h b/src/editor.h index 04620e9..13af934 100644 --- a/src/editor.h +++ b/src/editor.h @@ -29,6 +29,8 @@ public: void restoreViewState(const ViewState& vs); QsciScintilla* scintilla() const { return m_sci; } + QWidget* historyPopup() const { return m_historyPopup; } + QWidget* disasmPopup() const { return m_disasmPopup; } QWidget* structPreviewPopup() const { return m_structPreviewPopup; } const LineMeta* metaForLine(int line) const; int currentNodeIndex() const; diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index ef83788..9111982 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -2768,6 +2768,125 @@ private slots: "Static fields should not have a separator line"); } } + + // ── Test: disasm popup dismisses when mouse moves onto it ("see-through") ── + // + // Scenario: hover a FuncPtr row → disasm popup appears below the row. + // User moves mouse down onto the popup. The popup covers rows behind it + // but the mouse position maps to a different node's row in the viewport + // underneath, so the popup must dismiss. + void testDisasmPopupDismissesOnMouseMoveThrough() { + NodeTree tree; + tree.baseAddress = 0; + + Node root; + root.kind = NodeKind::Struct; + root.structTypeName = "TestClass"; + root.name = "TestClass"; + root.parentId = 0; + root.offset = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + // FuncPtr64 at offset 0 — its value points to "code" at byte 256 + Node fp; + fp.kind = NodeKind::FuncPtr64; + fp.name = "VFunc1"; + fp.parentId = rootId; + fp.offset = 0; + tree.addNode(fp); + + // A plain UInt64 after it so there's a non-FuncPtr row below + Node pad; + pad.kind = NodeKind::UInt64; + pad.name = "padding"; + pad.parentId = rootId; + pad.offset = 8; + tree.addNode(pad); + + // Buffer layout: + // [0..7] FuncPtr value = 256 (points to code bytes) + // [8..15] padding field value + // [256..383] x86 code bytes (push rbp; mov rbp,rsp; nop...; ret) + QByteArray data(512, '\0'); + uint64_t codeAddr = 256; + memcpy(data.data(), &codeAddr, 8); + const uint8_t code[] = { + 0x55, // push rbp + 0x48, 0x89, 0xE5, // mov rbp, rsp + 0x90, // nop + 0x90, // nop + 0x5D, // pop rbp + 0xC3 // ret + }; + memcpy(data.data() + 256, code, sizeof(code)); + BufferProvider prov(data, "test_disasm_dismiss"); + + ComposeResult cr = compose(tree, prov); + m_editor->applyDocument(cr); + m_editor->setProviderRef(&prov, nullptr, &tree); + QApplication::processEvents(); + + // Find the FuncPtr line + int fpLine = -1; + for (int i = 0; i < cr.meta.size(); ++i) { + if (isFuncPtr(cr.meta[i].nodeKind)) { + fpLine = i; + break; + } + } + QVERIFY2(fpLine >= 0, "Could not find FuncPtr64 line in compose output"); + + // Hover over the FuncPtr value column to trigger the disasm popup + const LineMeta& lm = cr.meta[fpLine]; + QString lineText; + { + long len = m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)fpLine); + QByteArray buf(len + 1, '\0'); + m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_GETLINE, (uintptr_t)fpLine, + static_cast(buf.data())); + lineText = QString::fromUtf8(buf.left(len)); + } + ColumnSpan vs = m_editor->valueSpan(lm, lineText.size(), + lm.effectiveTypeW, lm.effectiveNameW); + QVERIFY2(vs.valid, "Value span for FuncPtr line is not valid"); + + int hoverCol = (vs.start + vs.end) / 2; + QPoint vpFP = colToViewport(m_editor->scintilla(), fpLine, hoverCol); + sendMouseMove(m_editor->scintilla()->viewport(), vpFP); + QApplication::processEvents(); + + QWidget* popup = m_editor->disasmPopup(); + QVERIFY2(popup && popup->isVisible(), + "Disasm popup should be visible after hovering the FuncPtr value"); + + // See-through behavior: when the user moves the mouse down from the + // viewport onto the popup, the popup's mouseMoveEvent override forwards + // the global position back to the viewport hover logic. If the row + // underneath the popup represents a different node, the popup dismisses. + // + // Simulate by sending a MouseMove event to the popup at a global + // position that maps to the CommandRow (line 0) — a non-FuncPtr row. + // sendEvent triggers the virtual mouseMoveEvent directly. + QPoint vpCmdRow = colToViewport(m_editor->scintilla(), 0, hoverCol); + QPoint globalCmdRow = m_editor->scintilla()->viewport()->mapToGlobal(vpCmdRow); + QPoint localOnPopup = popup->mapFromGlobal(globalCmdRow); + QMouseEvent moveOnPopup(QEvent::MouseMove, + QPointF(localOnPopup), QPointF(globalCmdRow), + Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QApplication::sendEvent(popup, &moveOnPopup); + QApplication::processEvents(); + + QVERIFY2(!popup->isVisible(), + "Disasm popup must dismiss when mouseMoveEvent forwards " + "to a non-FuncPtr row underneath (see-through behavior)"); + + // Restore + m_editor->setProviderRef(nullptr, nullptr, nullptr); + m_editor->applyDocument(m_result); + } }; QTEST_MAIN(TestEditor)