mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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.
This commit is contained in:
112
src/editor.cpp
112
src/editor.cpp
@@ -40,18 +40,30 @@ class ValueHistoryPopup : public QFrame {
|
||||
QStringList m_values;
|
||||
QVector<QLabel*> m_labels;
|
||||
std::function<void(const QString&)> m_onSet;
|
||||
std::function<void(QMouseEvent*)> 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<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
||||
void setOnMouseMove(std::function<void(QMouseEvent*)> 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<void(QMouseEvent*)> 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<void(QMouseEvent*)> 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<void(QMouseEvent*)> 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<void(QMouseEvent*)> 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<ValueHistoryPopup*>(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<ValueHistoryPopup*>(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<ValueHistoryPopup*>(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<ValueHistoryPopup*>(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<DisasmPopup*>(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<DisasmPopup*>(
|
||||
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<StructPreviewPopup*>(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<StructPreviewPopup*>(m_structPreviewPopup);
|
||||
popup->populate(lm.nodeId,
|
||||
lm.pointerTargetName, body, editorFont());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<const char*>(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)
|
||||
|
||||
Reference in New Issue
Block a user