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:
IChooseYou
2026-03-03 08:31:49 -07:00
committed by IChooseYou
parent 6768f04e9a
commit b2ae8d5a5d
5 changed files with 205 additions and 32 deletions

View File

@@ -244,6 +244,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
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
connect(editor, &RcxEditor::inlineEditCommitted,
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())
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) {
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) 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,
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)
int padOffset = baseOffset;
while (gap > 0) {
@@ -741,8 +762,19 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
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,
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}));
}
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) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
const Node& node = m_doc->tree.nodes[nodeIdx];
@@ -1558,6 +1615,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
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
NodeKind commonKind = NodeKind::Hex64;
bool allSame = true;
@@ -1640,8 +1708,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
for (int ci : m_doc->tree.subtreeIndices(id))
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
}
for (auto& lm : m_lastResult.meta)
if (!m_valueHistory.contains(lm.nodeId)) lm.heatLevel = 0;
m_refreshGen++; // discard in-flight async reads
m_prevPages.clear(); // clean baseline for next read cycle
m_changedOffsets.clear(); // no phantom change indicators
m_valueTrackCooldown = 5; // suppress tracking for ~1s
refresh();
});
}
@@ -1674,7 +1744,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
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;
for (uint64_t id : ids) {
int ni = m_doc->tree.indexOfId(id);
@@ -1691,6 +1762,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
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) ──
if (hasNode) {
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);
for (int ci : m_doc->tree.subtreeIndices(nodeId))
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
for (auto& lm : m_lastResult.meta)
if (!m_valueHistory.contains(lm.nodeId)) lm.heatLevel = 0;
m_refreshGen++; // discard in-flight async reads
m_prevPages.clear(); // clean baseline for next read cycle
m_changedOffsets.clear(); // no phantom change indicators
m_valueTrackCooldown = 5; // suppress tracking for ~1s
refresh();
});
}
@@ -1993,24 +2088,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
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();
} // else (non-member node actions)
}
@@ -2091,10 +2168,26 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator();
menu.addAction(icon("clippy.svg"), "Copy All as Text", [editor]() {
QApplication::clipboard()->setText(editor->textWithMargins());
});
menu.addAction(icon("clippy.svg"), "Copy Line", [editor, line]() {
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
if (hasNode) {
uint64_t copyNodeId = m_doc->tree.nodes[nodeIdx].id;
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();
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
if (len > 0) {
@@ -2105,11 +2198,14 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
QApplication::clipboard()->setText(text);
}
});
copyMenu->addAction("Copy All as Text", [editor]() {
QApplication::clipboard()->setText(editor->textWithMargins());
});
menu.addSeparator();
menu.addAction(icon("search.svg"), "Search...", [editor]() {
editor->showFindBar();
menu.addAction(icon("search.svg"), "Search...\tCtrl+F", [editor]() {
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
});
menu.exec(globalPos);

View File

@@ -90,6 +90,7 @@ public:
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
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 toggleCollapse(int nodeIdx);
void materializeRefChildren(int nodeIdx);
@@ -147,8 +148,9 @@ public:
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
// Test accessor
// Test accessors
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
const ComposeResult& lastResult() const { return m_lastResult; }
signals:
void nodeSelected(int nodeIdx);
@@ -181,6 +183,7 @@ private:
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = true;
int m_valueTrackCooldown = 0;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;

View File

@@ -1471,7 +1471,12 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
int typeW = lm.effectiveTypeW;
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)
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
@@ -2242,6 +2247,12 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
case Qt::Key_Return:
case Qt::Key_Enter:
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: {
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,

View File

@@ -80,6 +80,7 @@ signals:
void inlineEditCancelled();
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;

View File

@@ -815,6 +815,68 @@ private slots:
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() {
uint64_t rootId = m_doc->tree.nodes[0].id;