mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: insert above node, clear value history cooldown, search context menu
- Insert 4/8 now inserts above the right-clicked node and shifts siblings down instead of appending at end. Insert key shortcut (Shift+Ins = 4, Ins = 8). Falls back to append when clicking empty space. - Clear Value History uses a 5-cycle cooldown counter so heat stays gone for ~1s instead of returning on the next async refresh. - Right-click Search defers showFindBar via QTimer::singleShot so focus isn't stolen by the closing context menu.
This commit is contained in:
@@ -244,6 +244,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
showTypePopup(editor, mode, nodeIdx, globalPos);
|
showTypePopup(editor, mode, nodeIdx, globalPos);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Insert key shortcut
|
||||||
|
connect(editor, &RcxEditor::insertAboveRequested,
|
||||||
|
this, [this](int nodeIdx, NodeKind kind) {
|
||||||
|
if (nodeIdx >= 0)
|
||||||
|
insertNodeAbove(nodeIdx, kind, QStringLiteral("field"));
|
||||||
|
else {
|
||||||
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
insertNode(target, -1, kind, QStringLiteral("field"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Inline editing signals
|
// Inline editing signals
|
||||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||||
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
|
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
|
||||||
@@ -591,7 +602,8 @@ void RcxController::refresh() {
|
|||||||
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
|
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
|
||||||
prov = m_doc->provider.get();
|
prov = m_doc->provider.get();
|
||||||
|
|
||||||
if (m_trackValues && prov) {
|
if (m_valueTrackCooldown > 0) --m_valueTrackCooldown;
|
||||||
|
if (m_trackValues && prov && m_valueTrackCooldown <= 0) {
|
||||||
for (auto& lm : m_lastResult.meta) {
|
for (auto& lm : m_lastResult.meta) {
|
||||||
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||||
if (isSyntheticLine(lm) || lm.isContinuation) continue;
|
if (isSyntheticLine(lm) || lm.isContinuation) continue;
|
||||||
@@ -708,6 +720,15 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
|||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
|
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
|
||||||
|
|
||||||
|
// Hex nodes don't display names (ASCII preview instead), so the stored
|
||||||
|
// name may be empty or stale. Give it a sensible default.
|
||||||
|
if (isHexNode(node.kind) && !isHexNode(newKind)) {
|
||||||
|
QString autoName = QStringLiteral("field_%1")
|
||||||
|
.arg(node.offset, 4, 16, QChar('0'));
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Rename{node.id, node.name, autoName}));
|
||||||
|
}
|
||||||
|
|
||||||
// Insert hex nodes to fill the gap (largest first for alignment)
|
// Insert hex nodes to fill the gap (largest first for alignment)
|
||||||
int padOffset = baseOffset;
|
int padOffset = baseOffset;
|
||||||
while (gap > 0) {
|
while (gap > 0) {
|
||||||
@@ -741,8 +762,19 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
|||||||
adjs.append({sib.id, sib.offset, sib.offset + delta});
|
adjs.append({sib.id, sib.offset, sib.offset + delta});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool needsRename = isHexNode(node.kind) && !isHexNode(newKind);
|
||||||
|
if (needsRename) {
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Change type"));
|
||||||
|
}
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeKind{node.id, node.kind, newKind, adjs}));
|
cmd::ChangeKind{node.id, node.kind, newKind, adjs}));
|
||||||
|
if (needsRename) {
|
||||||
|
QString autoName = QStringLiteral("field_%1")
|
||||||
|
.arg(node.offset, 4, 16, QChar('0'));
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Rename{node.id, node.name, autoName}));
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,6 +814,31 @@ void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, con
|
|||||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name) {
|
||||||
|
if (beforeIdx < 0 || beforeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
|
const Node& before = m_doc->tree.nodes[beforeIdx];
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = kind;
|
||||||
|
n.name = name;
|
||||||
|
n.parentId = before.parentId;
|
||||||
|
n.offset = before.offset;
|
||||||
|
n.id = m_doc->tree.reserveId();
|
||||||
|
|
||||||
|
int insertSize = sizeForKind(kind);
|
||||||
|
|
||||||
|
// Shift siblings at or after the insertion offset down
|
||||||
|
QVector<cmd::OffsetAdj> adjs;
|
||||||
|
auto siblings = m_doc->tree.childrenOf(before.parentId);
|
||||||
|
for (int si : siblings) {
|
||||||
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
|
if (sib.offset >= before.offset)
|
||||||
|
adjs.append({sib.id, sib.offset, sib.offset + insertSize});
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::removeNode(int nodeIdx) {
|
void RcxController::removeNode(int nodeIdx) {
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
@@ -1558,6 +1615,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
return indices;
|
return indices;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Insert shortcuts (always available) ──
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
||||||
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
||||||
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
// Quick-convert shortcuts when all selected nodes share the same kind
|
// Quick-convert shortcuts when all selected nodes share the same kind
|
||||||
NodeKind commonKind = NodeKind::Hex64;
|
NodeKind commonKind = NodeKind::Hex64;
|
||||||
bool allSame = true;
|
bool allSame = true;
|
||||||
@@ -1640,8 +1708,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
for (int ci : m_doc->tree.subtreeIndices(id))
|
for (int ci : m_doc->tree.subtreeIndices(id))
|
||||||
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
||||||
}
|
}
|
||||||
for (auto& lm : m_lastResult.meta)
|
m_refreshGen++; // discard in-flight async reads
|
||||||
if (!m_valueHistory.contains(lm.nodeId)) lm.heatLevel = 0;
|
m_prevPages.clear(); // clean baseline for next read cycle
|
||||||
|
m_changedOffsets.clear(); // no phantom change indicators
|
||||||
|
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1674,7 +1744,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
menu.addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
||||||
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
||||||
QStringList addrs;
|
QStringList addrs;
|
||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
int ni = m_doc->tree.indexOfId(id);
|
int ni = m_doc->tree.indexOfId(id);
|
||||||
@@ -1691,6 +1762,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
QMenu menu;
|
QMenu menu;
|
||||||
|
|
||||||
|
// ── Insert shortcuts (at very top) ──
|
||||||
|
if (hasNode) {
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 4 Above\tShift+Ins",
|
||||||
|
[this, nodeIdx]() {
|
||||||
|
insertNodeAbove(nodeIdx, NodeKind::Hex32, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 8 Above\tIns",
|
||||||
|
[this, nodeIdx]() {
|
||||||
|
insertNodeAbove(nodeIdx, NodeKind::Hex64, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
||||||
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
||||||
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
// ── Node-specific actions (only when clicking on a node) ──
|
// ── Node-specific actions (only when clicking on a node) ──
|
||||||
if (hasNode) {
|
if (hasNode) {
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
@@ -1839,8 +1932,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
m_valueHistory.remove(nodeId);
|
m_valueHistory.remove(nodeId);
|
||||||
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
||||||
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
||||||
for (auto& lm : m_lastResult.meta)
|
m_refreshGen++; // discard in-flight async reads
|
||||||
if (!m_valueHistory.contains(lm.nodeId)) lm.heatLevel = 0;
|
m_prevPages.clear(); // clean baseline for next read cycle
|
||||||
|
m_changedOffsets.clear(); // no phantom change indicators
|
||||||
|
m_valueTrackCooldown = 5; // suppress tracking for ~1s
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1993,24 +2088,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
if (ni >= 0) removeNode(ni);
|
if (ni >= 0) removeNode(ni);
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
|
||||||
|
|
||||||
menu.addAction(icon("link.svg"), "Copy &Address", [this, nodeId]() {
|
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
|
||||||
if (ni < 0) return;
|
|
||||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
|
||||||
QApplication::clipboard()->setText(
|
|
||||||
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.addAction(icon("whole-word.svg"), "Copy &Offset", [this, nodeId]() {
|
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
|
||||||
if (ni < 0) return;
|
|
||||||
int off = m_doc->tree.nodes[ni].offset;
|
|
||||||
QApplication::clipboard()->setText(
|
|
||||||
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
|
|
||||||
});
|
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
} // else (non-member node actions)
|
} // else (non-member node actions)
|
||||||
}
|
}
|
||||||
@@ -2091,10 +2168,26 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
menu.addAction(icon("clippy.svg"), "Copy All as Text", [editor]() {
|
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
||||||
QApplication::clipboard()->setText(editor->textWithMargins());
|
if (hasNode) {
|
||||||
});
|
uint64_t copyNodeId = m_doc->tree.nodes[nodeIdx].id;
|
||||||
menu.addAction(icon("clippy.svg"), "Copy Line", [editor, line]() {
|
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, copyNodeId]() {
|
||||||
|
int ni = m_doc->tree.indexOfId(copyNodeId);
|
||||||
|
if (ni < 0) return;
|
||||||
|
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||||
|
QApplication::clipboard()->setText(
|
||||||
|
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
||||||
|
});
|
||||||
|
copyMenu->addAction(icon("whole-word.svg"), "Copy &Offset", [this, copyNodeId]() {
|
||||||
|
int ni = m_doc->tree.indexOfId(copyNodeId);
|
||||||
|
if (ni < 0) return;
|
||||||
|
int off = m_doc->tree.nodes[ni].offset;
|
||||||
|
QApplication::clipboard()->setText(
|
||||||
|
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
|
||||||
|
});
|
||||||
|
copyMenu->addSeparator();
|
||||||
|
}
|
||||||
|
copyMenu->addAction("Copy Line", [editor, line]() {
|
||||||
auto* sci = editor->scintilla();
|
auto* sci = editor->scintilla();
|
||||||
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
@@ -2105,11 +2198,14 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
QApplication::clipboard()->setText(text);
|
QApplication::clipboard()->setText(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
copyMenu->addAction("Copy All as Text", [editor]() {
|
||||||
|
QApplication::clipboard()->setText(editor->textWithMargins());
|
||||||
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
menu.addAction(icon("search.svg"), "Search...", [editor]() {
|
menu.addAction(icon("search.svg"), "Search...\tCtrl+F", [editor]() {
|
||||||
editor->showFindBar();
|
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.exec(globalPos);
|
menu.exec(globalPos);
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ public:
|
|||||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||||
void renameNode(int nodeIdx, const QString& newName);
|
void renameNode(int nodeIdx, const QString& newName);
|
||||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||||
|
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
||||||
void removeNode(int nodeIdx);
|
void removeNode(int nodeIdx);
|
||||||
void toggleCollapse(int nodeIdx);
|
void toggleCollapse(int nodeIdx);
|
||||||
void materializeRefChildren(int nodeIdx);
|
void materializeRefChildren(int nodeIdx);
|
||||||
@@ -147,8 +148,9 @@ public:
|
|||||||
// Cross-tab type visibility: point at the project's full document list
|
// Cross-tab type visibility: point at the project's full document list
|
||||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||||
|
|
||||||
// Test accessor
|
// Test accessors
|
||||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||||
|
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
@@ -181,6 +183,7 @@ private:
|
|||||||
QSet<int64_t> m_changedOffsets;
|
QSet<int64_t> m_changedOffsets;
|
||||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||||
bool m_trackValues = true;
|
bool m_trackValues = true;
|
||||||
|
int m_valueTrackCooldown = 0;
|
||||||
uint64_t m_refreshGen = 0;
|
uint64_t m_refreshGen = 0;
|
||||||
uint64_t m_readGen = 0;
|
uint64_t m_readGen = 0;
|
||||||
bool m_readInFlight = false;
|
bool m_readInFlight = false;
|
||||||
|
|||||||
@@ -1471,7 +1471,12 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
|||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int nameW = lm.effectiveNameW;
|
int nameW = lm.effectiveNameW;
|
||||||
|
|
||||||
if (heat <= 0) continue;
|
if (heat <= 0) {
|
||||||
|
// Clear any stale heat indicators from a previous frame
|
||||||
|
for (int hi : heatIndicators)
|
||||||
|
clearIndicatorLine(hi, i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||||
@@ -2242,6 +2247,12 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
|||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
return beginInlineEdit(EditTarget::Value);
|
return beginInlineEdit(EditTarget::Value);
|
||||||
|
case Qt::Key_Insert:
|
||||||
|
if (ke->modifiers() & Qt::ShiftModifier)
|
||||||
|
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex32);
|
||||||
|
else
|
||||||
|
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex64);
|
||||||
|
return true;
|
||||||
case Qt::Key_Tab: {
|
case Qt::Key_Tab: {
|
||||||
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
||||||
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ signals:
|
|||||||
void inlineEditCancelled();
|
void inlineEditCancelled();
|
||||||
void typeSelectorRequested();
|
void typeSelectorRequested();
|
||||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||||
|
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||||
|
|||||||
@@ -815,6 +815,68 @@ private slots:
|
|||||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: clearing value history actually resets heat to 0 ──
|
||||||
|
void testClearValueHistoryResetsHeat() {
|
||||||
|
// Use a live provider so value tracking runs during refresh()
|
||||||
|
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0);
|
||||||
|
m_ctrl->setTrackValues(true);
|
||||||
|
|
||||||
|
// Do initial refresh to populate m_lastResult.meta
|
||||||
|
m_ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find field_u32 nodeId
|
||||||
|
uint64_t targetId = 0;
|
||||||
|
for (const auto& n : m_doc->tree.nodes) {
|
||||||
|
if (n.name == "field_u32") { targetId = n.id; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(targetId != 0);
|
||||||
|
|
||||||
|
// Seed value history with multiple changes to get heat > 0
|
||||||
|
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||||
|
history[targetId].record("val_1");
|
||||||
|
history[targetId].record("val_2");
|
||||||
|
history[targetId].record("val_3");
|
||||||
|
QVERIFY2(history[targetId].heatLevel() >= 2,
|
||||||
|
"Pre-clear: should have heat >= 2 (warm)");
|
||||||
|
|
||||||
|
// Refresh so heatLevel propagates to LineMeta
|
||||||
|
m_ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Verify heat is visible in meta
|
||||||
|
bool foundHot = false;
|
||||||
|
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||||
|
if (lm.nodeId == targetId && lm.heatLevel > 0) {
|
||||||
|
foundHot = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(foundHot, "Pre-clear: LineMeta should show heat > 0");
|
||||||
|
|
||||||
|
// Now simulate what the "Clear Value History" context menu does:
|
||||||
|
// remove from history map + clear subtree + refresh
|
||||||
|
history.remove(targetId);
|
||||||
|
for (int ci : m_doc->tree.subtreeIndices(targetId))
|
||||||
|
history.remove(m_doc->tree.nodes[ci].id);
|
||||||
|
|
||||||
|
m_ctrl->refresh();
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// After clear + refresh, heatLevel must be 0 for this node
|
||||||
|
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||||
|
if (lm.nodeId == targetId) {
|
||||||
|
QCOMPARE(lm.heatLevel, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The history entry should exist again (re-recorded by refresh)
|
||||||
|
// but with only 1 unique value → heatLevel 0
|
||||||
|
QVERIFY(history.contains(targetId));
|
||||||
|
QCOMPARE(history[targetId].heatLevel(), 0);
|
||||||
|
QCOMPARE(history[targetId].uniqueCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
void testStaticFieldTypeChangePreservesFlags() {
|
void testStaticFieldTypeChangePreservesFlags() {
|
||||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user