mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Fix 7 data bugs: hex endian, signed hex, array span, UTF-8 positions, array undo, offset sign
This commit is contained in:
@@ -18,6 +18,7 @@ struct ComposeState {
|
||||
int currentLine = 0;
|
||||
int typeW = kColType; // global type column width (fallback)
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
bool baseEmitted = false; // only first root struct shows base address
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
@@ -206,7 +207,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG);
|
||||
lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct);
|
||||
lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted);
|
||||
if (lm.isRootHeader) state.baseEmitted = true;
|
||||
|
||||
QString headerText;
|
||||
if (node.kind == NodeKind::Array) {
|
||||
@@ -350,7 +352,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
}
|
||||
maxTypeLen = qMax(maxTypeLen, typeName.size());
|
||||
}
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen + 1, kMaxTypeW);
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
|
||||
|
||||
// Compute effective name column width from longest name
|
||||
int maxNameLen = kMinNameW;
|
||||
@@ -361,7 +363,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
|
||||
maxNameLen = qMax(maxNameLen, node.name.size());
|
||||
}
|
||||
state.nameW = qBound(kMinNameW, maxNameLen + 1, kMaxNameW);
|
||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||
|
||||
// Pre-compute per-scope widths (each container gets widths based on direct children only)
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
@@ -375,24 +377,43 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
for (int childIdx : state.childMap.value(container.id)) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
|
||||
// Skip containers - their headers don't use columnar layout
|
||||
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
continue;
|
||||
|
||||
// Type width
|
||||
QString childTypeName;
|
||||
if (child.kind == NodeKind::Array) {
|
||||
childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen);
|
||||
} else {
|
||||
childTypeName = fmt::typeNameRaw(child.kind);
|
||||
}
|
||||
QString childTypeName = fmt::typeNameRaw(child.kind);
|
||||
scopeMaxType = qMax(scopeMaxType, childTypeName.size());
|
||||
|
||||
// Name width (skip hex/padding and containers)
|
||||
if (!isHexPreview(child.kind) &&
|
||||
child.kind != NodeKind::Struct && child.kind != NodeKind::Array) {
|
||||
// Name width (skip hex/padding)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
scopeMaxName = qMax(scopeMaxName, child.name.size());
|
||||
}
|
||||
}
|
||||
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType + 1, kMaxTypeW);
|
||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName + 1, kMaxNameW);
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
|
||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
// Compute scope widths for root level (parentId == 0)
|
||||
{
|
||||
int rootMaxType = kMinTypeW;
|
||||
int rootMaxName = kMinNameW;
|
||||
for (int childIdx : state.childMap.value(0)) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
|
||||
// Skip containers - their headers don't use columnar layout
|
||||
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
continue;
|
||||
|
||||
QString childTypeName = fmt::typeNameRaw(child.kind);
|
||||
rootMaxType = qMax(rootMaxType, childTypeName.size());
|
||||
if (!isHexPreview(child.kind)) {
|
||||
rootMaxName = qMax(rootMaxName, child.name.size());
|
||||
}
|
||||
}
|
||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
|
||||
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
QVector<int> roots = state.childMap.value(0);
|
||||
|
||||
@@ -151,13 +151,12 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
bool typeOk;
|
||||
NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk);
|
||||
if (typeOk && nodeIdx < m_doc->tree.nodes.size()) {
|
||||
Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
if (node.kind == NodeKind::Array) {
|
||||
// Update element kind and count (no undo for now)
|
||||
node.elementKind = elemKind;
|
||||
node.arrayLen = newCount;
|
||||
if (node.viewIndex >= newCount)
|
||||
node.viewIndex = qMax(0, newCount - 1);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeArrayMeta{node.id,
|
||||
node.elementKind, elemKind,
|
||||
node.arrayLen, newCount}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,6 +445,14 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||
m_doc->provider->writeBytes(c.addr, bytes);
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0) {
|
||||
tree.nodes[idx].elementKind = isUndo ? c.oldElementKind : c.newElementKind;
|
||||
tree.nodes[idx].arrayLen = isUndo ? c.oldArrayLen : c.newArrayLen;
|
||||
if (tree.nodes[idx].viewIndex >= tree.nodes[idx].arrayLen)
|
||||
tree.nodes[idx].viewIndex = qMax(0, tree.nodes[idx].arrayLen - 1);
|
||||
}
|
||||
}
|
||||
}, command);
|
||||
|
||||
|
||||
33
src/core.h
33
src/core.h
@@ -382,6 +382,12 @@ struct NodeTree {
|
||||
|
||||
int structSpan(uint64_t structId,
|
||||
const QHash<uint64_t, QVector<int>>* childMap = nullptr) const {
|
||||
int idx = indexOfId(structId);
|
||||
if (idx < 0) return 0;
|
||||
|
||||
const Node& node = nodes[idx];
|
||||
int declaredSize = node.byteSize();
|
||||
|
||||
int maxEnd = 0;
|
||||
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
@@ -391,7 +397,8 @@ struct NodeTree {
|
||||
int end = c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
}
|
||||
return maxEnd;
|
||||
|
||||
return qMax(declaredSize, maxEnd);
|
||||
}
|
||||
|
||||
// Batch selection normalizers
|
||||
@@ -480,11 +487,15 @@ namespace cmd {
|
||||
QVector<OffsetAdj> offAdjs; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
||||
struct ChangeArrayMeta { uint64_t nodeId;
|
||||
NodeKind oldElementKind, newElementKind;
|
||||
int oldArrayLen, newArrayLen; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -504,7 +515,7 @@ inline constexpr int kColName = 22;
|
||||
inline constexpr int kColValue = 32;
|
||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||
inline constexpr int kSepWidth = 2;
|
||||
inline constexpr int kSepWidth = 1;
|
||||
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
|
||||
inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]")
|
||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||
@@ -541,7 +552,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 4);
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
int start = ind + prefixW;
|
||||
return {start, start + valWidth, true};
|
||||
}
|
||||
@@ -564,7 +575,7 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 4);
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
start = ind + prefixW + valWidth;
|
||||
} else {
|
||||
start = isHexPad
|
||||
@@ -575,13 +586,13 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
}
|
||||
|
||||
// Base address span (only valid for root struct headers)
|
||||
// Line format: " - struct Name { base: 0x00400000"
|
||||
// Line format: " - struct Name { // base: 0x00400000"
|
||||
inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {};
|
||||
// Find "base: " after the opening brace
|
||||
int baseIdx = lineText.indexOf(QStringLiteral("base: "));
|
||||
// Find "// base: " after the opening brace
|
||||
int baseIdx = lineText.indexOf(QStringLiteral("// base: "));
|
||||
if (baseIdx < 0) return {};
|
||||
int startPos = baseIdx + 6; // after "base: "
|
||||
int startPos = baseIdx + 9; // after "// base: "
|
||||
// Value goes to end of line
|
||||
int endPos = lineText.size();
|
||||
while (endPos > startPos && lineText[endPos-1].isSpace())
|
||||
@@ -590,10 +601,10 @@ inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText
|
||||
return {startPos, endPos, true};
|
||||
}
|
||||
|
||||
// Full "base: 0x..." span for coloring (includes "base: " prefix)
|
||||
// Full "// base: 0x..." span for coloring (includes "// base: " prefix)
|
||||
inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {};
|
||||
int baseIdx = lineText.indexOf(QStringLiteral("base: "));
|
||||
int baseIdx = lineText.indexOf(QStringLiteral("// base: "));
|
||||
if (baseIdx < 0) return {};
|
||||
int endPos = lineText.size();
|
||||
while (endPos > baseIdx && lineText[endPos-1].isSpace())
|
||||
|
||||
@@ -140,7 +140,7 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_BASE_ADDR, QColor("#6a9955"));
|
||||
IND_BASE_ADDR, QColor("#5a8248"));
|
||||
|
||||
// Hover span indicator — blue text like a link
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
@@ -517,10 +517,8 @@ void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
ColumnSpan span = baseAddressFullSpanFor(lm, lineText);
|
||||
if (!span.valid) continue;
|
||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)i);
|
||||
long posA = lineStart + span.start;
|
||||
long posB = lineStart + span.end;
|
||||
long posA = posFromCol(m_sci, i, span.start);
|
||||
long posB = posFromCol(m_sci, i, span.end);
|
||||
if (posB > posA)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
|
||||
}
|
||||
@@ -1381,11 +1379,9 @@ void RcxEditor::setEditComment(const QString& comment) {
|
||||
QString formatted = QStringLiteral("//") + comment;
|
||||
QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth);
|
||||
|
||||
// Use direct position calculation from line start
|
||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)m_editState.line);
|
||||
long posA = lineStart + startCol;
|
||||
long posB = lineStart + endCol;
|
||||
// Use UTF-8 safe column-to-position conversion
|
||||
long posA = posFromCol(m_sci, m_editState.line, startCol);
|
||||
long posB = posFromCol(m_sci, m_editState.line, endCol);
|
||||
|
||||
QByteArray utf8 = padded.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
||||
|
||||
@@ -10,7 +10,7 @@ static constexpr int COL_TYPE = kColType;
|
||||
static constexpr int COL_NAME = kColName;
|
||||
static constexpr int COL_VALUE = kColValue;
|
||||
static constexpr int COL_COMMENT = 28; // "// Enter=Save Esc=Cancel" fits
|
||||
static const QString SEP = QStringLiteral(" ");
|
||||
static const QString SEP = QStringLiteral(" ");
|
||||
|
||||
static QString fit(QString s, int w) {
|
||||
if (w <= 0) return {};
|
||||
@@ -94,6 +94,8 @@ QString indent(int depth) {
|
||||
|
||||
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) {
|
||||
if (isContinuation) return QStringLiteral(" \u00B7");
|
||||
if (relativeOffset < 0)
|
||||
return QStringLiteral("-0x") + QString::number(-relativeOffset, 16).toUpper();
|
||||
return QStringLiteral("+0x") + QString::number(relativeOffset, 16).toUpper();
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddres
|
||||
QString header = indent(depth) + typeName(node.kind).trimmed() +
|
||||
QStringLiteral(" ") + node.name + QStringLiteral(" { ");
|
||||
QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper();
|
||||
return header + QStringLiteral("base: ") + baseHex;
|
||||
return header + QStringLiteral("// base: ") + baseHex;
|
||||
}
|
||||
|
||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
@@ -255,7 +257,7 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
QString type = typeName(node.kind, colType);
|
||||
QString name = fit(node.name, colName);
|
||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||
const int prefixW = colType + colName + 4; // 2 seps × 2 chars
|
||||
const int prefixW = colType + colName + 2 * kSepWidth;
|
||||
|
||||
// Comment suffix (padded or empty)
|
||||
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
|
||||
@@ -384,13 +386,62 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
||||
|
||||
switch (kind) {
|
||||
case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok);
|
||||
case NodeKind::Hex16: return parseHexBytes(stripHex(s), 2, ok);
|
||||
case NodeKind::Hex32: return parseHexBytes(stripHex(s), 4, ok);
|
||||
case NodeKind::Hex64: return parseHexBytes(stripHex(s), 8, ok);
|
||||
case NodeKind::Int8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked<int8_t>(val, ok); }
|
||||
case NodeKind::Int16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return parseIntChecked<int16_t>(val, ok); }
|
||||
case NodeKind::Int32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; int val = stripHex(s).toInt(ok,b); return *ok ? toBytes<int32_t>(val) : QByteArray{}; }
|
||||
case NodeKind::Int64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qlonglong val = stripHex(s).toLongLong(ok,b); return *ok ? toBytes<int64_t>(val) : QByteArray{}; }
|
||||
case NodeKind::Hex16: {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
if (*ok && val > 0xFFFF) *ok = false;
|
||||
return *ok ? toBytes<uint16_t>(static_cast<uint16_t>(val)) : QByteArray{};
|
||||
}
|
||||
case NodeKind::Hex32: {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::Hex64: {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::Int8: {
|
||||
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
||||
if (isHex) {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
if (*ok && val > 0xFF) *ok = false;
|
||||
return *ok ? toBytes<int8_t>(static_cast<int8_t>(val)) : QByteArray{};
|
||||
} else {
|
||||
int val = s.toInt(ok, 10);
|
||||
return parseIntChecked<int8_t>(val, ok);
|
||||
}
|
||||
}
|
||||
case NodeKind::Int16: {
|
||||
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
||||
if (isHex) {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
if (*ok && val > 0xFFFF) *ok = false;
|
||||
return *ok ? toBytes<int16_t>(static_cast<int16_t>(val)) : QByteArray{};
|
||||
} else {
|
||||
int val = s.toInt(ok, 10);
|
||||
return parseIntChecked<int16_t>(val, ok);
|
||||
}
|
||||
}
|
||||
case NodeKind::Int32: {
|
||||
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
||||
if (isHex) {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
if (*ok && val > 0xFFFFFFFFULL) *ok = false;
|
||||
return *ok ? toBytes<int32_t>(static_cast<int32_t>(val)) : QByteArray{};
|
||||
} else {
|
||||
int val = s.toInt(ok, 10);
|
||||
return *ok ? toBytes<int32_t>(val) : QByteArray{};
|
||||
}
|
||||
}
|
||||
case NodeKind::Int64: {
|
||||
bool isHex = s.startsWith("0x", Qt::CaseInsensitive);
|
||||
if (isHex) {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<int64_t>(static_cast<int64_t>(val)) : QByteArray{};
|
||||
} else {
|
||||
qlonglong val = s.toLongLong(ok, 10);
|
||||
return *ok ? toBytes<int64_t>(val) : QByteArray{};
|
||||
}
|
||||
}
|
||||
case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint8_t>(val, ok); }
|
||||
case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked<uint16_t>(val, ok); }
|
||||
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
|
||||
|
||||
Reference in New Issue
Block a user