feat: track value changes toggle, hover scroll fix, ptr* convert, hex split

This commit is contained in:
IChooseYou
2026-02-19 06:32:58 -07:00
committed by IChooseYou
parent 26217f5de8
commit acc3ebf5db
4 changed files with 267 additions and 1 deletions

View File

@@ -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<Node> 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<Node> 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]() {

View File

@@ -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<int>& nodeIndices);
void batchChangeKind(const QVector<int>& 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<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
@@ -154,6 +160,7 @@ private:
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = false;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;

View File

@@ -20,6 +20,7 @@
#include <QLabel>
#include <QToolButton>
#include <QScreen>
#include <QScrollBar>
#include <functional>
#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,

View File

@@ -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)