diff --git a/src/controller.cpp b/src/controller.cpp index 5f1a0eb..98d7c5e 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -604,6 +604,16 @@ void RcxController::scrollToNodeId(uint64_t nodeId) { editor->scrollToNodeId(nodeId); } +void RcxController::setTrackValues(bool on) { + m_trackValues = on; + if (!on) { + m_valueHistory.clear(); + for (auto& lm : m_lastResult.meta) + lm.heatLevel = 0; + refresh(); + } +} + void RcxController::refresh() { // Bracket compose with thread-local doc pointer for type name resolution s_composeDoc = m_doc; @@ -656,7 +666,7 @@ void RcxController::refresh() { else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive()) prov = m_doc->provider.get(); - if (prov) { + if (m_trackValues && prov) { for (auto& lm : m_lastResult.meta) { if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue; if (isSyntheticLine(lm) || lm.isContinuation) continue; @@ -1181,6 +1191,128 @@ void RcxController::duplicateNode(int nodeIdx) { m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs})); } +void RcxController::convertToTypedPointer(uint64_t nodeId) { + int ni = m_doc->tree.indexOfId(nodeId); + if (ni < 0) return; + const Node& node = m_doc->tree.nodes[ni]; + + // Determine pointer kind from current node size + NodeKind ptrKind; + if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64) + ptrKind = NodeKind::Pointer64; + else + ptrKind = NodeKind::Pointer32; + + // Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ... + QString baseName = QStringLiteral("NewClass"); + QString typeName = baseName; + int suffix = 2; + while (true) { + bool exists = false; + for (const auto& n : m_doc->tree.nodes) { + if (n.kind == NodeKind::Struct && n.structTypeName == typeName) { + exists = true; break; + } + } + if (!exists) break; + typeName = QStringLiteral("%1_%2").arg(baseName).arg(suffix++); + } + + // Create the new root struct node + Node rootStruct; + rootStruct.kind = NodeKind::Struct; + rootStruct.name = QStringLiteral("instance"); + rootStruct.structTypeName = typeName; + rootStruct.classKeyword = QStringLiteral("class"); + rootStruct.parentId = 0; + rootStruct.offset = 0; + rootStruct.id = m_doc->tree.reserveId(); + + // Create child Hex64 fields for the new struct + constexpr int kDefaultFields = 16; + QVector children; + for (int i = 0; i < kDefaultFields; i++) { + Node c; + c.kind = NodeKind::Hex64; + c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0')); + c.parentId = rootStruct.id; + c.offset = i * 8; + c.id = m_doc->tree.reserveId(); + children.append(c); + } + + uint64_t oldRefId = node.refId; + + m_suppressRefresh = true; + m_doc->undoStack.beginMacro(QStringLiteral("Change to ptr*")); + + // 1. Change kind to Pointer64/32 (if not already) + if (node.kind != ptrKind) + changeNodeKind(ni, ptrKind); + + // 2. Insert the new root struct + m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{rootStruct, {}})); + + // 3. Insert its children + for (const Node& c : children) + m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{c, {}})); + + // 4. Set refId to point to the new struct + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangePointerRef{nodeId, oldRefId, rootStruct.id})); + + m_doc->undoStack.endMacro(); + m_suppressRefresh = false; + refresh(); +} + +void RcxController::splitHexNode(uint64_t nodeId) { + int ni = m_doc->tree.indexOfId(nodeId); + if (ni < 0) return; + const Node& node = m_doc->tree.nodes[ni]; + + NodeKind halfKind; + int halfSize; + if (node.kind == NodeKind::Hex64) { halfKind = NodeKind::Hex32; halfSize = 4; } + else if (node.kind == NodeKind::Hex32) { halfKind = NodeKind::Hex16; halfSize = 2; } + else if (node.kind == NodeKind::Hex16) { halfKind = NodeKind::Hex8; halfSize = 1; } + else return; + + uint64_t parentId = node.parentId; + int baseOffset = node.offset; + QString baseName = node.name; + + m_suppressRefresh = true; + m_doc->undoStack.beginMacro(QStringLiteral("Split Hex node")); + + // Remove the original node + QVector subtree; + subtree.append(node); + m_doc->undoStack.push(new RcxCommand(this, + cmd::Remove{nodeId, subtree, {}})); + + // Insert two half-sized nodes + Node lo; + lo.kind = halfKind; + lo.name = baseName; + lo.parentId = parentId; + lo.offset = baseOffset; + lo.id = m_doc->tree.reserveId(); + m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{lo, {}})); + + Node hi; + hi.kind = halfKind; + hi.name = baseName + QStringLiteral("_hi"); + hi.parentId = parentId; + hi.offset = baseOffset + halfSize; + hi.id = m_doc->tree.reserveId(); + m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{hi, {}})); + + m_doc->undoStack.endMacro(); + m_suppressRefresh = false; + refresh(); +} + void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos) { auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); }; @@ -1278,6 +1410,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, batchChangeKind(collectIndices(), kindFromString(sel)); }); + menu.addSeparator(); + { + auto* act = menu.addAction("Track Value Changes"); + act->setCheckable(true); + act->setChecked(m_trackValues); + connect(act, &QAction::toggled, this, &RcxController::setTrackValues); + } menu.addSeparator(); menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() { @@ -1368,6 +1507,32 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, }); addedQuickConvert = true; } + // "Change to ptr*" — convert hex/void-ptr to typed pointer with auto-created class + if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Hex32 + || ((node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32) + && node.refId == 0)) { + menu.addAction("Change to ptr*", [this, nodeId]() { + convertToTypedPointer(nodeId); + }); + addedQuickConvert = true; + } + // Split hex node into two half-sized hex nodes + if (node.kind == NodeKind::Hex64) { + menu.addAction("Change to hex32+hex32", [this, nodeId]() { + splitHexNode(nodeId); + }); + addedQuickConvert = true; + } else if (node.kind == NodeKind::Hex32) { + menu.addAction("Change to hex16+hex16", [this, nodeId]() { + splitHexNode(nodeId); + }); + addedQuickConvert = true; + } else if (node.kind == NodeKind::Hex16) { + menu.addAction("Change to hex8+hex8", [this, nodeId]() { + splitHexNode(nodeId); + }); + addedQuickConvert = true; + } if (addedQuickConvert) menu.addSeparator(); @@ -1388,6 +1553,15 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, editor->beginInlineEdit(EditTarget::Type, line); }); + menu.addSeparator(); + { + auto* act = menu.addAction("Track Value Changes"); + act->setCheckable(true); + act->setChecked(m_trackValues); + connect(act, &QAction::toggled, this, &RcxController::setTrackValues); + } + menu.addSeparator(); + // Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8) if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) { menu.addAction("Convert to &Hex", [this, nodeId]() { @@ -1497,6 +1671,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, refresh(); }); + menu.addSeparator(); + { + auto* act = menu.addAction("Track Value Changes"); + act->setCheckable(true); + act->setChecked(m_trackValues); + connect(act, &QAction::toggled, this, &RcxController::setTrackValues); + } menu.addSeparator(); menu.addAction(icon("arrow-left.svg"), "Undo", [this]() { diff --git a/src/controller.h b/src/controller.h index adbea5b..8b7b874 100644 --- a/src/controller.h +++ b/src/controller.h @@ -94,6 +94,8 @@ public: void materializeRefChildren(int nodeIdx); void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false); void duplicateNode(int nodeIdx); + void convertToTypedPointer(uint64_t nodeId); + void splitHexNode(uint64_t nodeId); void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos); void batchRemoveNodes(const QVector& nodeIndices); void batchChangeKind(const QVector& nodeIndices, NodeKind newKind); @@ -123,6 +125,10 @@ public: int activeSourceIndex() const { return m_activeSourceIdx; } void switchSource(int idx) { switchToSavedSource(idx); } + // Value tracking toggle (per-tab, off by default) + bool trackValues() const { return m_trackValues; } + void setTrackValues(bool on); + // Test accessor const QHash& valueHistory() const { return m_valueHistory; } @@ -154,6 +160,7 @@ private: PageMap m_prevPages; QSet m_changedOffsets; QHash m_valueHistory; + bool m_trackValues = false; uint64_t m_refreshGen = 0; uint64_t m_readGen = 0; bool m_readInFlight = false; diff --git a/src/editor.cpp b/src/editor.cpp index f937ae8..551a442 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "themes/thememanager.h" @@ -394,6 +395,24 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { m_sci->viewport()->installEventFilter(this); m_sci->viewport()->setMouseTracking(true); + // Recalculate hover when the viewport scrolls (scrollbar drag, wheel + // deceleration, etc.) so the highlight tracks whatever is under the cursor. + connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged, + this, [this]() { + if (m_editState.active || !m_hoverInside) return; + m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos()); + m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos); + auto h = hitTest(m_lastHoverPos); + uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; + int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1; + if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) { + m_hoveredNodeId = newHoverId; + m_hoveredLine = newHoverLine; + applyHoverHighlight(); + } + applyHoverCursor(); + }); + // Hover cursor is applied synchronously in eventFilter (no timer). connect(m_sci, &QsciScintilla::marginClicked, diff --git a/tests/test_context_menu.cpp b/tests/test_context_menu.cpp index c2695e0..d90a2e4 100644 --- a/tests/test_context_menu.cpp +++ b/tests/test_context_menu.cpp @@ -394,6 +394,65 @@ private slots: QApplication::processEvents(); QCOMPARE(countNodes(), before); } + + // ── Change to Ptr* creates class and sets refId ── + + void testChangeToPtrStarCreatesClassAndSetsRef() { + // Add a Hex64 node to the root struct + uint64_t rootId = m_doc->tree.nodes[0].id; + m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField"); + QApplication::processEvents(); + + int ptrIdx = findNode("ptrField"); + QVERIFY(ptrIdx >= 0); + uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id; + int before = countNodes(); + + // Convert to typed pointer + m_ctrl->convertToTypedPointer(ptrNodeId); + QApplication::processEvents(); + + // Re-find after tree mutation + ptrIdx = -1; + for (int i = 0; i < m_doc->tree.nodes.size(); i++) { + if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; } + } + QVERIFY(ptrIdx >= 0); + + // Verify: node kind changed to Pointer64 + QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64); + + // Verify: node.refId != 0 + uint64_t refId = m_doc->tree.nodes[ptrIdx].refId; + QVERIFY(refId != 0); + + // Verify: a new Struct node exists with the refId as its id + int structIdx = m_doc->tree.indexOfId(refId); + QVERIFY(structIdx >= 0); + QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct); + + // Verify: the new struct has children (Hex64 fields) + auto children = m_doc->tree.childrenOf(refId); + QVERIFY(children.size() == 16); + for (int ci : children) + QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64); + + // Verify: total nodes increased by 1 struct + 16 children = 17 + QCOMPARE(countNodes(), before + 17); + + // Verify: undo restores the original Hex64 kind and refId==0 + m_doc->undoStack.undo(); + QApplication::processEvents(); + + ptrIdx = -1; + for (int i = 0; i < m_doc->tree.nodes.size(); i++) { + if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; } + } + QVERIFY(ptrIdx >= 0); + QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64); + QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0); + QCOMPARE(countNodes(), before); + } }; QTEST_MAIN(TestContextMenu)