mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: track value changes toggle, hover scroll fix, ptr* convert, hex split
This commit is contained in:
@@ -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]() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user