mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: fix heatmap false-heat on offset shift, hover flicker, type chooser cleanup
- Clear value history when node offsets change (insert/delete/resize/ manual offset edit) so stale values from old addresses don't show false heat coloring - Invalidate in-flight async reads (bump refreshGen) when tree layout changes, preventing stale snapshot data from re-introducing heat - Fix command bar hover cursor flicker: remove premature applyHoverCursor() from applyDocument() — runs correctly via applySelectionOverlays() after text is finalized - Fix hover indicator survival: reorder refresh() so text-modifying passes (updateCommandRow) run before overlay passes - Guard synthetic Leave events during setText() to preserve hover state - Remove primitives from type chooser when pointer modifier (* / **) is active; remove primitives entirely in Root command bar mode - Add test_editor and test_controller test coverage for heat clearing, hover survival, and mixed hex/non-hex type scenarios
This commit is contained in:
@@ -436,7 +436,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
else
|
||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
|
||||
@@ -634,12 +637,12 @@ void RcxController::refresh() {
|
||||
}
|
||||
|
||||
// Update value history and compute heat levels
|
||||
// Use the snapshot provider if active; skip entirely if no valid provider
|
||||
// Only run when a live provider is attached (not for static file/buffer sources)
|
||||
{
|
||||
const Provider* prov = nullptr;
|
||||
if (m_snapshotProv)
|
||||
if (m_snapshotProv && m_snapshotProv->isLive())
|
||||
prov = m_snapshotProv.get();
|
||||
else if (m_doc->provider && m_doc->provider->isValid())
|
||||
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
|
||||
prov = m_doc->provider.get();
|
||||
|
||||
if (prov) {
|
||||
@@ -651,11 +654,9 @@ void RcxController::refresh() {
|
||||
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
||||
// Skip containers — they don't have scalar values
|
||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
|
||||
// Skip hex preview nodes — they show raw bytes, not a single value
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
|
||||
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(nodeOff);
|
||||
uint64_t addr = static_cast<uint64_t>(nodeOff); // provider-relative
|
||||
int sz = node.byteSize();
|
||||
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
||||
|
||||
@@ -666,17 +667,6 @@ void RcxController::refresh() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply persisted heat levels even when provider is unavailable
|
||||
if (!prov) {
|
||||
for (auto& lm : m_lastResult.meta) {
|
||||
if (lm.nodeId != 0) {
|
||||
auto it = m_valueHistory.find(lm.nodeId);
|
||||
if (it != m_valueHistory.end())
|
||||
lm.heatLevel = it->heatLevel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||
@@ -707,9 +697,11 @@ void RcxController::refresh() {
|
||||
editor->applyDocument(m_lastResult);
|
||||
editor->restoreViewState(vs);
|
||||
}
|
||||
applySelectionOverlays();
|
||||
// Text-modifying passes first (command row replaces line 0 text),
|
||||
// then overlays last so hover indicators survive the refresh.
|
||||
pushSavedSourcesToEditors();
|
||||
updateCommandRow();
|
||||
applySelectionOverlays();
|
||||
}
|
||||
|
||||
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
||||
@@ -906,6 +898,23 @@ void RcxController::materializeRefChildren(int nodeIdx) {
|
||||
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
auto& tree = m_doc->tree;
|
||||
|
||||
// Clear value history for nodes whose effective offset changed.
|
||||
// When offsets shift (insert/delete/resize), old recorded values came from
|
||||
// a different memory address, so keeping them would show false heat.
|
||||
// Also invalidates any in-flight async read so that stale snapshot data
|
||||
// from before the offset change doesn't re-introduce false heat.
|
||||
auto clearHistoryForAdjs = [&](const QVector<cmd::OffsetAdj>& adjs) {
|
||||
if (adjs.isEmpty()) return;
|
||||
m_refreshGen++; // discard in-flight async read (stale layout)
|
||||
for (const auto& adj : adjs) {
|
||||
// Clear the adjusted node itself
|
||||
m_valueHistory.remove(adj.nodeId);
|
||||
// Clear all descendants (their effective address also shifted)
|
||||
for (int ci : tree.subtreeIndices(adj.nodeId))
|
||||
m_valueHistory.remove(tree.nodes[ci].id);
|
||||
}
|
||||
};
|
||||
|
||||
std::visit([&](auto&& c) {
|
||||
using T = std::decay_t<decltype(c)>;
|
||||
if constexpr (std::is_same_v<T, cmd::ChangeKind>) {
|
||||
@@ -917,6 +926,12 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
if (ai >= 0)
|
||||
tree.nodes[ai].offset = isUndo ? adj.oldOffset : adj.newOffset;
|
||||
}
|
||||
// The changed node's value format changed; clear its history.
|
||||
// If offAdjs is empty (same-size change), still bump gen to
|
||||
// discard in-flight reads that would record the old format.
|
||||
if (c.offAdjs.isEmpty()) m_refreshGen++;
|
||||
m_valueHistory.remove(c.nodeId);
|
||||
clearHistoryForAdjs(c.offAdjs);
|
||||
} else if constexpr (std::is_same_v<T, cmd::Rename>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
@@ -945,6 +960,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||
}
|
||||
}
|
||||
clearHistoryForAdjs(c.offAdjs);
|
||||
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
||||
if (isUndo) {
|
||||
// Restore nodes first
|
||||
@@ -970,6 +986,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
}
|
||||
tree.invalidateIdCache();
|
||||
}
|
||||
// Siblings shifted — their old values are from wrong addresses
|
||||
clearHistoryForAdjs(c.offAdjs);
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
||||
tree.baseAddress = isUndo ? c.oldBase : c.newBase;
|
||||
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
|
||||
@@ -1016,6 +1034,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
tree.nodes[idx].offset = isUndo ? c.oldOffset : c.newOffset;
|
||||
// Node and its descendants read from a different address now
|
||||
m_refreshGen++; // discard in-flight async read (stale layout)
|
||||
m_valueHistory.remove(c.nodeId);
|
||||
for (int ci : tree.subtreeIndices(c.nodeId))
|
||||
m_valueHistory.remove(tree.nodes[ci].id);
|
||||
}
|
||||
}, command);
|
||||
|
||||
@@ -1494,8 +1517,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
}
|
||||
}
|
||||
|
||||
applySelectionOverlays();
|
||||
updateCommandRow();
|
||||
applySelectionOverlays();
|
||||
|
||||
if (m_selIds.size() == 1) {
|
||||
uint64_t sid = *m_selIds.begin();
|
||||
@@ -1508,8 +1531,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
void RcxController::clearSelection() {
|
||||
m_selIds.clear();
|
||||
m_anchorLine = -1;
|
||||
applySelectionOverlays();
|
||||
updateCommandRow();
|
||||
applySelectionOverlays();
|
||||
}
|
||||
|
||||
void RcxController::applySelectionOverlays() {
|
||||
@@ -1750,7 +1773,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
|
||||
switch (mode) {
|
||||
case TypePopupMode::Root:
|
||||
addPrimitives(/*enabled=*/false, /*excludeStructArrayPad=*/false);
|
||||
// No primitives in Root mode – only project types are valid roots
|
||||
addComposites([&](const Node&, const TypeEntry& e) {
|
||||
return e.structId == m_viewRootId;
|
||||
});
|
||||
@@ -2019,7 +2042,10 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
else
|
||||
m_doc->provider->setBase(m_doc->tree.baseAddress);
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
refresh();
|
||||
@@ -2062,9 +2088,15 @@ void RcxController::pushSavedSourcesToEditors() {
|
||||
|
||||
// ── Auto-refresh ──
|
||||
|
||||
void RcxController::setRefreshInterval(int ms) {
|
||||
if (m_refreshTimer)
|
||||
m_refreshTimer->setInterval(qMax(1, ms));
|
||||
}
|
||||
|
||||
void RcxController::setupAutoRefresh() {
|
||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
m_refreshTimer = new QTimer(this);
|
||||
m_refreshTimer->setInterval(660);
|
||||
m_refreshTimer->setInterval(qMax(1, ms));
|
||||
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
||||
m_refreshTimer->start();
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ public:
|
||||
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -121,6 +122,9 @@ public:
|
||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||
|
||||
// Test accessor
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
void selectionChanged(int count);
|
||||
|
||||
299
src/editor.cpp
299
src/editor.cpp
@@ -5,6 +5,7 @@
|
||||
#include <Qsci/qsciscintillabase.h>
|
||||
#include <Qsci/qscilexercpp.h>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFont>
|
||||
#include <QColor>
|
||||
#include <QKeyEvent>
|
||||
@@ -15,10 +16,142 @@
|
||||
#include <QMenu>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QScreen>
|
||||
#include <functional>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Value history popup (styled like TypeSelectorPopup) ──
|
||||
|
||||
class ValueHistoryPopup : public QFrame {
|
||||
uint64_t m_nodeId = 0;
|
||||
bool m_hasButtons = false;
|
||||
QStringList m_values;
|
||||
QVector<QLabel*> m_labels;
|
||||
std::function<void(const QString&)> m_onSet;
|
||||
public:
|
||||
explicit ValueHistoryPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
uint64_t nodeId() const { return m_nodeId; }
|
||||
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
||||
|
||||
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
||||
bool showButtons = false) {
|
||||
QStringList vals;
|
||||
hist.forEach([&](const QString& v) { vals.append(v); });
|
||||
|
||||
if (nodeId == m_nodeId && vals == m_values
|
||||
&& showButtons == m_hasButtons && isVisible())
|
||||
return;
|
||||
|
||||
// In-place label update when structure unchanged (avoids flicker)
|
||||
if (nodeId == m_nodeId && vals.size() == m_values.size()
|
||||
&& vals.size() == m_labels.size()
|
||||
&& showButtons == m_hasButtons && isVisible()) {
|
||||
for (int i = 0; i < vals.size(); i++)
|
||||
m_labels[i]->setText(vals[i]);
|
||||
m_values = vals;
|
||||
return;
|
||||
}
|
||||
|
||||
m_nodeId = nodeId;
|
||||
m_values = vals;
|
||||
m_hasButtons = showButtons;
|
||||
m_labels.clear();
|
||||
|
||||
delete layout();
|
||||
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
vbox->setSpacing(2);
|
||||
|
||||
auto* title = new QLabel(QStringLiteral("Previous Values"));
|
||||
QFont bold = font;
|
||||
bold.setBold(true);
|
||||
title->setFont(bold);
|
||||
title->setStyleSheet(QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
vbox->addWidget(title);
|
||||
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
sep->setFixedHeight(1);
|
||||
QPalette sp; sp.setColor(QPalette::WindowText, theme.border);
|
||||
sep->setPalette(sp);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
for (const QString& v : vals) {
|
||||
auto* row = new QHBoxLayout;
|
||||
row->setContentsMargins(0, 1, 0, 1);
|
||||
row->setSpacing(8);
|
||||
|
||||
auto* label = new QLabel(v);
|
||||
label->setFont(font);
|
||||
label->setStyleSheet(QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
||||
row->addWidget(label, 1);
|
||||
m_labels.append(label);
|
||||
|
||||
if (showButtons) {
|
||||
auto* setBtn = new QToolButton;
|
||||
setBtn->setText(QStringLiteral("Set"));
|
||||
setBtn->setAutoRaise(true);
|
||||
setBtn->setCursor(Qt::PointingHandCursor);
|
||||
setBtn->setFont(font);
|
||||
setBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 1px 4px; }"
|
||||
"QToolButton:hover { color: %2; background: %3; }")
|
||||
.arg(theme.textDim.name(), theme.text.name(), theme.hover.name()));
|
||||
QString val = v;
|
||||
QObject::connect(setBtn, &QToolButton::clicked, [this, val]() {
|
||||
if (m_onSet) m_onSet(val);
|
||||
});
|
||||
row->addWidget(setBtn);
|
||||
}
|
||||
vbox->addLayout(row);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos) {
|
||||
if (isVisible()) return;
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - 4;
|
||||
move(x, y);
|
||||
show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
m_values.clear();
|
||||
m_labels.clear();
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr int IND_EDITABLE = 8;
|
||||
static constexpr int IND_HEX_DIM = 9;
|
||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||
@@ -71,6 +204,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_sci, &QWidget::customContextMenuRequested,
|
||||
this, [this](const QPoint& pos) {
|
||||
// Right-click on offset margin → show margin mode menu
|
||||
int margin0Width = (int)m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
||||
if (pos.x() < margin0Width) {
|
||||
QMenu menu;
|
||||
auto* actRel = menu.addAction("Relative Offsets (+0x)");
|
||||
auto* actAbs = menu.addAction("Absolute Addresses");
|
||||
actRel->setCheckable(true);
|
||||
actAbs->setCheckable(true);
|
||||
actRel->setChecked(m_relativeOffsets);
|
||||
actAbs->setChecked(!m_relativeOffsets);
|
||||
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
|
||||
if (chosen == actRel && !m_relativeOffsets) {
|
||||
m_relativeOffsets = true;
|
||||
reformatMargins();
|
||||
} else if (chosen == actAbs && m_relativeOffsets) {
|
||||
m_relativeOffsets = false;
|
||||
reformatMargins();
|
||||
}
|
||||
return;
|
||||
}
|
||||
int line = m_sci->lineAt(pos);
|
||||
int nodeIdx = -1;
|
||||
int subLine = 0;
|
||||
@@ -374,6 +528,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
if (m_editState.active)
|
||||
endInlineEdit();
|
||||
|
||||
// Guard: suppress popup dismiss during setText() which fires synthetic Leave events
|
||||
m_applyingDocument = true;
|
||||
|
||||
// Save hover state — setText() triggers viewport Leave events that would clear it
|
||||
uint64_t savedHoverId = m_hoveredNodeId;
|
||||
int savedHoverLine = m_hoveredLine;
|
||||
@@ -422,6 +579,13 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_hoveredNodeId = savedHoverId;
|
||||
m_hoveredLine = savedHoverLine;
|
||||
m_hoverInside = savedHoverInside;
|
||||
m_applyingDocument = false;
|
||||
|
||||
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
||||
// composed text that updateCommandRow() will overwrite. The correct call
|
||||
// happens via applySelectionOverlays() after all text is finalized.
|
||||
applyHoverHighlight();
|
||||
}
|
||||
|
||||
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||
@@ -787,31 +951,35 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
|
||||
// For hex preview nodes: use dataChanged + changedByteIndices (per-byte heat)
|
||||
if (heat <= 0) continue;
|
||||
|
||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||
|
||||
// For hex preview nodes: per-byte heat coloring on changed bytes
|
||||
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
|
||||
// Hex nodes don't track heatLevel (they're skipped in controller).
|
||||
// Use IND_HEAT_COLD for any changed byte (simple visual feedback).
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int asciiStart = ind + typeW + kSepWidth;
|
||||
int hexStart = asciiStart + nameW + kSepWidth;
|
||||
|
||||
for (int byteIdx : lm.changedByteIndices) {
|
||||
fillIndicatorCols(IND_HEAT_COLD, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||
fillIndicatorCols(activeInd, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||
int hexCol = hexStart + byteIdx * 3;
|
||||
fillIndicatorCols(IND_HEAT_COLD, i, hexCol, hexCol + 2);
|
||||
fillIndicatorCols(activeInd, i, hexCol, hexCol + 2);
|
||||
}
|
||||
// Clear the other two heat indicators on this line
|
||||
for (int hi : heatIndicators) {
|
||||
if (hi != activeInd)
|
||||
clearIndicatorLine(hi, i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-hex nodes: apply heat-level indicator to value span
|
||||
if (heat <= 0) continue;
|
||||
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
||||
if (!vs.valid) continue;
|
||||
|
||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||
fillIndicatorCols(activeInd, i, vs.start, vs.end);
|
||||
|
||||
// Clear the other two heat indicators on this span to avoid overlap
|
||||
@@ -1478,6 +1646,10 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
}
|
||||
// Track mouse position for cursor updates (both edit and non-edit mode)
|
||||
if (obj == m_sci->viewport()) {
|
||||
// Ignore synthetic Leave from setText() during document refresh
|
||||
if (m_applyingDocument && event->type() == QEvent::Leave)
|
||||
return true;
|
||||
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
m_lastHoverPos = static_cast<QMouseEvent*>(event)->pos();
|
||||
m_hoverInside = true;
|
||||
@@ -1683,6 +1855,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
// Dismiss hover popup so it gets recreated with Set buttons once edit starts
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||
m_hintLine = -1;
|
||||
@@ -1864,6 +2039,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
});
|
||||
}
|
||||
// Refresh hover cursor so value history popup appears with Set buttons immediately
|
||||
if (target == EditTarget::Value)
|
||||
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2216,25 +2394,60 @@ void RcxEditor::applyHoverCursor() {
|
||||
if (m_editState.active) {
|
||||
if (m_sci->isListActive()) {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
auto h = hitTest(m_lastHoverPos);
|
||||
if (h.line == m_editState.line &&
|
||||
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
||||
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
||||
} else {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
auto h = hitTest(m_lastHoverPos);
|
||||
if (h.line == m_editState.line &&
|
||||
h.col >= m_editState.spanStart && h.col <= editEndCol()) {
|
||||
m_sci->viewport()->setCursor(Qt::IBeamCursor);
|
||||
} else {
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
}
|
||||
// Value history popup — only during inline value editing on a heated node
|
||||
{
|
||||
bool showPopup = false;
|
||||
if (m_valueHistory && m_editState.target == EditTarget::Value
|
||||
&& m_editState.line >= 0 && m_editState.line < m_meta.size()) {
|
||||
const LineMeta& lm = m_meta[m_editState.line];
|
||||
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||
auto it = m_valueHistory->find(lm.nodeId);
|
||||
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||
if (!m_historyPopup)
|
||||
m_historyPopup = new ValueHistoryPopup(this);
|
||||
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||
popup->setOnSet([this](const QString& val) {
|
||||
if (!m_editState.active) return;
|
||||
long endPos = posFromCol(m_sci, m_editState.line, editEndCol());
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
||||
m_editState.posStart, endPos);
|
||||
QByteArray utf8 = val.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||
(uintptr_t)0, utf8.constData());
|
||||
});
|
||||
popup->populate(lm.nodeId, *it, editorFont(), true);
|
||||
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||
(unsigned long)0, m_editState.posStart);
|
||||
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||
(unsigned long)0, m_editState.posStart);
|
||||
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||
(unsigned long)m_editState.line);
|
||||
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
||||
popup->showAt(anchor);
|
||||
showPopup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse left viewport - set Arrow, cancel calltip
|
||||
// Mouse left viewport - set Arrow, dismiss history popup
|
||||
// (but not during applyDocument — the Leave is synthetic from setText)
|
||||
if (!m_hoverInside) {
|
||||
if (m_calltipVisible) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
|
||||
m_calltipVisible = false;
|
||||
m_calltipLine = -1;
|
||||
}
|
||||
if (m_historyPopup && !m_applyingDocument)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
@@ -2323,41 +2536,39 @@ void RcxEditor::applyHoverCursor() {
|
||||
m_hoverSpanLines.append(h.line);
|
||||
}
|
||||
|
||||
// Value history calltip on hover
|
||||
// Value history popup on hover (read-only, no buttons)
|
||||
{
|
||||
bool showCalltip = false;
|
||||
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size() && !m_editState.active) {
|
||||
bool showPopup = false;
|
||||
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
|
||||
const LineMeta& lm = m_meta[h.line];
|
||||
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||
auto it = m_valueHistory->find(lm.nodeId);
|
||||
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||
// Check cursor is over the value span
|
||||
QString lineText = getLineText(m_sci, h.line);
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||
QString tip = QStringLiteral("Previous Values:");
|
||||
it->forEach([&](const QString& v) {
|
||||
tip += QStringLiteral("\n ") + v;
|
||||
});
|
||||
if (m_calltipLine != h.line) {
|
||||
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
if (!m_historyPopup)
|
||||
m_historyPopup = new ValueHistoryPopup(this);
|
||||
auto* popup = static_cast<ValueHistoryPopup*>(m_historyPopup);
|
||||
popup->populate(lm.nodeId, *it, editorFont(), false);
|
||||
long linePos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
QByteArray tipUtf8 = tip.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPSHOW,
|
||||
pos, tipUtf8.constData());
|
||||
m_calltipLine = h.line;
|
||||
m_calltipVisible = true;
|
||||
}
|
||||
showCalltip = true;
|
||||
long byteOff = lineText.left(vs.start).toUtf8().size();
|
||||
int px = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION,
|
||||
(unsigned long)0, linePos + byteOff);
|
||||
int py = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTYFROMPOSITION,
|
||||
(unsigned long)0, linePos);
|
||||
int lh = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT,
|
||||
(unsigned long)h.line);
|
||||
QPoint anchor = m_sci->viewport()->mapToGlobal(QPoint(px, py + lh));
|
||||
popup->showAt(anchor);
|
||||
showPopup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showCalltip && m_calltipVisible) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
|
||||
m_calltipVisible = false;
|
||||
m_calltipLine = -1;
|
||||
}
|
||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Determine cursor shape based on interaction type
|
||||
|
||||
@@ -132,10 +132,10 @@ private:
|
||||
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
bool m_calltipVisible = false;
|
||||
int m_calltipLine = -1;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
bool m_updatingComment = false;
|
||||
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
{
|
||||
"baseAddress": "400000",
|
||||
"nextId": "29",
|
||||
"nodes": [
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "1",
|
||||
"kind": "Struct",
|
||||
"name": "aBall",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "ball"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "2",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "3",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "4",
|
||||
"kind": "Vec4",
|
||||
"name": "position",
|
||||
"offset": 16,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "5",
|
||||
"kind": "Vec3",
|
||||
"name": "velocity",
|
||||
"offset": 32,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "6",
|
||||
"kind": "Hex32",
|
||||
"name": "field_2C",
|
||||
"offset": 44,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "7",
|
||||
"kind": "Float",
|
||||
"name": "speed",
|
||||
"offset": 48,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "8",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 52,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "9",
|
||||
"kind": "Float",
|
||||
"name": "radius",
|
||||
"offset": 56,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "10",
|
||||
"kind": "Hex32",
|
||||
"name": "field_3C",
|
||||
"offset": 60,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "11",
|
||||
"kind": "Float",
|
||||
"name": "mass",
|
||||
"offset": 64,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "12",
|
||||
"kind": "Hex64",
|
||||
"name": "field_44",
|
||||
"offset": 68,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "13",
|
||||
"kind": "Bool",
|
||||
"name": "bouncy",
|
||||
"offset": 76,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "14",
|
||||
"kind": "Hex8",
|
||||
"name": "field_4D",
|
||||
"offset": 77,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "15",
|
||||
"kind": "Hex16",
|
||||
"name": "field_4E",
|
||||
"offset": 78,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "16",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 80,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "17",
|
||||
"kind": "Hex32",
|
||||
"name": "field_54",
|
||||
"offset": 84,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "18",
|
||||
"kind": "Hex64",
|
||||
"name": "field_58",
|
||||
"offset": 88,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "19",
|
||||
"kind": "Hex64",
|
||||
"name": "field_60",
|
||||
"offset": 96,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "20",
|
||||
"kind": "Struct",
|
||||
"name": "aPhysics",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "Physics"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "21",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "22",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "23",
|
||||
"kind": "Hex64",
|
||||
"name": "field_10",
|
||||
"offset": 16,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "24",
|
||||
"kind": "Hex64",
|
||||
"name": "field_18",
|
||||
"offset": 24,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "25",
|
||||
"kind": "Hex64",
|
||||
"name": "field_20",
|
||||
"offset": 32,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": true,
|
||||
"elementKind": "UInt8",
|
||||
"id": "26",
|
||||
"kind": "Pointer64",
|
||||
"name": "physics",
|
||||
"offset": 104,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 4,
|
||||
"collapsed": false,
|
||||
"elementKind": "Float",
|
||||
"id": "27",
|
||||
"kind": "Array",
|
||||
"name": "scores",
|
||||
"offset": 112,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 2,
|
||||
"collapsed": false,
|
||||
"elementKind": "Struct",
|
||||
"id": "28",
|
||||
"kind": "Array",
|
||||
"name": "materials",
|
||||
"offset": 128,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
123
src/main.cpp
123
src/main.cpp
@@ -412,6 +412,18 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||
// Examples submenu — scan once at init
|
||||
{
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||
if (!rcxFiles.isEmpty()) {
|
||||
auto* examples = file->addMenu("&Examples");
|
||||
for (const QString& fn : rcxFiles) {
|
||||
QString fullPath = exDir.absoluteFilePath(fn);
|
||||
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
|
||||
}
|
||||
}
|
||||
}
|
||||
file->addSeparator();
|
||||
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||
@@ -732,77 +744,22 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
return sub;
|
||||
}
|
||||
|
||||
// Build Ball + Material demo structs into a tree
|
||||
static void buildBallDemo(NodeTree& tree) {
|
||||
// Ball struct (128 bytes = 0x80)
|
||||
Node ball;
|
||||
ball.kind = NodeKind::Struct;
|
||||
ball.name = "aBall";
|
||||
ball.structTypeName = "Ball";
|
||||
ball.parentId = 0;
|
||||
ball.offset = 0;
|
||||
int bi = tree.addNode(ball);
|
||||
uint64_t ballId = tree.nodes[bi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; tree.addNode(n); }
|
||||
|
||||
// Material struct (renamed from Physics, 40 bytes = 0x28)
|
||||
Node mat;
|
||||
mat.kind = NodeKind::Struct;
|
||||
mat.name = "aMaterial";
|
||||
mat.structTypeName = "Material";
|
||||
mat.parentId = 0;
|
||||
mat.offset = 0;
|
||||
int mi = tree.addNode(mat);
|
||||
uint64_t matId = tree.nodes[mi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = matId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = matId; n.offset = 8; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = matId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = matId; n.offset = 24; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = matId; n.offset = 32; tree.addNode(n); }
|
||||
|
||||
// Pointer to Material in Ball struct
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); }
|
||||
|
||||
// float[4] scores at offset 112
|
||||
{ Node n; n.kind = NodeKind::Array; n.name = "scores"; n.parentId = ballId; n.offset = 112; n.elementKind = NodeKind::Float; n.arrayLen = 4; tree.addNode(n); }
|
||||
|
||||
// Material[2] materials at offset 128 (112 + 16 for float[4])
|
||||
{ Node n; n.kind = NodeKind::Array; n.name = "materials"; n.parentId = ballId; n.offset = 128; n.elementKind = NodeKind::Struct; n.arrayLen = 2; n.refId = matId; tree.addNode(n); }
|
||||
|
||||
// Unnamed struct (128 bytes of hex64 fields)
|
||||
Node unnamed;
|
||||
unnamed.kind = NodeKind::Struct;
|
||||
unnamed.name = "instance";
|
||||
unnamed.structTypeName = "Unnamed";
|
||||
unnamed.parentId = 0;
|
||||
unnamed.offset = 0;
|
||||
int ui = tree.addNode(unnamed);
|
||||
uint64_t unnamedId = tree.nodes[ui].id;
|
||||
// Build a minimal empty struct for new documents
|
||||
static void buildEmptyStruct(NodeTree& tree) {
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "instance";
|
||||
root.structTypeName = "Unnamed";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
n.parentId = unnamedId;
|
||||
n.parentId = rootId;
|
||||
n.offset = i * 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
@@ -829,14 +786,12 @@ void MainWindow::newDocument() {
|
||||
doc->typeAliases.clear();
|
||||
doc->modified = false;
|
||||
|
||||
// Build Ball + Material structs
|
||||
buildBallDemo(doc->tree);
|
||||
buildEmptyStruct(doc->tree);
|
||||
|
||||
// Cross-platform writable buffer, zeroed (256 bytes covers Ball + spare)
|
||||
QByteArray data(256, '\0');
|
||||
doc->provider = std::make_shared<BufferProvider>(data);
|
||||
|
||||
// Focus on Ball struct
|
||||
// Focus on first struct
|
||||
ctrl->setViewRootId(0);
|
||||
for (const auto& n : doc->tree.nodes) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
@@ -854,7 +809,22 @@ void MainWindow::newDocument() {
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
project_new();
|
||||
// Auto-open KUSER_SHARED_DATA example if available
|
||||
QString exPath = QCoreApplication::applicationDirPath()
|
||||
+ "/examples/KUSER_SHARED_DATA.rcx";
|
||||
if (QFile::exists(exPath)) {
|
||||
project_open(exPath);
|
||||
} else {
|
||||
project_new();
|
||||
}
|
||||
|
||||
// Auto-attach process memory plugin to self
|
||||
auto* ctrl = activeController();
|
||||
if (ctrl) {
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::openFile() {
|
||||
@@ -1080,6 +1050,7 @@ void MainWindow::showOptionsDialog() {
|
||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
|
||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
|
||||
OptionsDialog dlg(current, this);
|
||||
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
||||
@@ -1107,6 +1078,12 @@ void MainWindow::showOptionsDialog() {
|
||||
|
||||
if (r.autoStartMcp != current.autoStartMcp)
|
||||
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
|
||||
|
||||
if (r.refreshMs != current.refreshMs) {
|
||||
QSettings("Reclass", "Reclass").setValue("refreshMs", r.refreshMs);
|
||||
for (auto& tab : m_tabs)
|
||||
tab.ctrl->setRefreshInterval(r.refreshMs);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setEditorFont(const QString& fontName) {
|
||||
@@ -1510,13 +1487,11 @@ void MainWindow::showTypeAliasesDialog() {
|
||||
QMdiSubWindow* MainWindow::project_new() {
|
||||
auto* doc = new RcxDocument(this);
|
||||
|
||||
// Cross-platform writable buffer, zeroed (256 bytes covers Ball struct + spare)
|
||||
QByteArray data(256, '\0');
|
||||
doc->loadData(data);
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
|
||||
// Build Ball + Material demo structs
|
||||
buildBallDemo(doc->tree);
|
||||
buildEmptyStruct(doc->tree);
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
|
||||
@@ -58,6 +58,29 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
generalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generalLayout->setSpacing(8);
|
||||
|
||||
// Refresh Rate group box
|
||||
auto* refreshGroup = new QGroupBox("Refresh Rate");
|
||||
auto* refreshLayout = new QFormLayout(refreshGroup);
|
||||
refreshLayout->setSpacing(8);
|
||||
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_refreshSpin = new QSpinBox;
|
||||
m_refreshSpin->setRange(1, 60000);
|
||||
m_refreshSpin->setSingleStep(50);
|
||||
m_refreshSpin->setValue(current.refreshMs);
|
||||
m_refreshSpin->setSuffix(" ms");
|
||||
m_refreshSpin->setObjectName("refreshSpin");
|
||||
refreshLayout->addRow("Interval:", m_refreshSpin);
|
||||
|
||||
auto* refreshDesc = new QLabel(
|
||||
"How often live memory is re-read and the view is updated, in milliseconds. "
|
||||
"Lower values give faster updates but use more CPU. Default: 660 ms.");
|
||||
refreshDesc->setWordWrap(true);
|
||||
refreshDesc->setContentsMargins(0, 0, 0, 0);
|
||||
refreshLayout->addRow(refreshDesc);
|
||||
|
||||
generalLayout->addWidget(refreshGroup);
|
||||
|
||||
// Visual Experience group box
|
||||
auto* visualGroup = new QGroupBox("Visual Experience");
|
||||
auto* visualLayout = new QFormLayout(visualGroup);
|
||||
@@ -184,6 +207,7 @@ OptionsResult OptionsDialog::result() const {
|
||||
r.showIcon = m_showIconCheck->isChecked();
|
||||
r.safeMode = m_safeModeCheck->isChecked();
|
||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||
r.refreshMs = m_refreshSpin->value();
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QHash>
|
||||
#include <QSpinBox>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -16,6 +17,7 @@ struct OptionsResult {
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = false;
|
||||
int refreshMs = 660;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
@@ -38,6 +40,7 @@ private:
|
||||
QCheckBox* m_showIconCheck = nullptr;
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
QSpinBox* m_refreshSpin = nullptr;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"indHoverSpan": "#E6B450",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#569cd6",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#E6B450",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"indHoverSpan": "#b180d7",
|
||||
"indCmdPill": "#2d2d30",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#569cd6",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#d69d85",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"indHoverSpan": "#AA9565",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#6B959F",
|
||||
"indHeatCold": "#6B959F",
|
||||
"indHeatCold": "#C4A44A",
|
||||
"indHeatWarm": "#AA9565",
|
||||
"indHeatHot": "#A05040",
|
||||
"indHintGreen": "#464646",
|
||||
|
||||
@@ -54,9 +54,9 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
||||
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||
}
|
||||
// Derive heat colors from the theme's own palette when keys are absent
|
||||
// cold = keyword blue, warm = hover/string amber, hot = marker red
|
||||
// cold = muted yellow, warm = hover/string amber, hot = marker red
|
||||
if (!t.indHeatCold.isValid())
|
||||
t.indHeatCold = t.syntaxKeyword;
|
||||
t.indHeatCold = QColor("#D4A945");
|
||||
if (!t.indHeatWarm.isValid())
|
||||
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||
if (!t.indHeatHot.isValid())
|
||||
|
||||
@@ -336,6 +336,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_arrayCountEdit->setVisible(id == 3);
|
||||
if (id == 3) m_arrayCountEdit->setFocus();
|
||||
updateModifierPreview();
|
||||
applyFilter(m_filterEdit->text());
|
||||
});
|
||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||
this, [this]() { updateModifierPreview(); });
|
||||
@@ -562,6 +563,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|
||||
QString filterBase = text.trimmed();
|
||||
|
||||
// Hide primitives when a pointer modifier (* or **) is active
|
||||
int modId = m_modGroup->checkedId();
|
||||
bool hideprimitives = (modId == 1 || modId == 2);
|
||||
|
||||
// Separate primitives and composites
|
||||
QVector<TypeEntry> primitives, composites;
|
||||
for (const auto& t : m_allTypes) {
|
||||
@@ -571,9 +576,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||
if (!matchesFilter) continue;
|
||||
|
||||
if (t.entryKind == TypeEntry::Primitive)
|
||||
primitives.append(t);
|
||||
else if (t.entryKind == TypeEntry::Composite)
|
||||
if (t.entryKind == TypeEntry::Primitive) {
|
||||
if (!hideprimitives)
|
||||
primitives.append(t);
|
||||
} else if (t.entryKind == TypeEntry::Composite)
|
||||
composites.append(t);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user