mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Type picker refactor: use SCI_USERLISTSHOW, fix coordinate handling
- Switch type picker from SCI_AUTOCSHOW to SCI_USERLISTSHOW for proper
signal handling (userListActivated now fires correctly)
- Fix coordinate system mixing by using posFromCol() consistently
- Skip 0x prefix in value/base address edit selection
- Dynamic hint positioning: 2 spaces after value with // prefix
- Clearer validation errors ("too large! max=0x..." instead of "max")
- Green colored hint text using IND_BASE_ADDR indicator
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,7 +115,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.isContinuation = isCont;
|
lm.isContinuation = isCont;
|
||||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
@@ -162,13 +162,19 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Header;
|
lm.lineKind = LineKind::Header;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
lm.foldLevel = computeFoldLevel(depth, true);
|
lm.foldLevel = computeFoldLevel(depth, true);
|
||||||
lm.markerMask = (1u << M_STRUCT_BG);
|
lm.markerMask = (1u << M_STRUCT_BG);
|
||||||
state.emitLine(fmt::fmtStructHeader(node, depth), lm);
|
lm.isRootHeader = (node.parentId == 0); // Root-level struct
|
||||||
|
|
||||||
|
// Root structs show base address, nested structs show normal header
|
||||||
|
QString headerText = lm.isRootHeader
|
||||||
|
? fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress)
|
||||||
|
: fmt::fmtStructHeader(node, depth);
|
||||||
|
state.emitLine(headerText, lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.collapsed) {
|
if (!node.collapsed) {
|
||||||
@@ -215,7 +221,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.nodeId = node.id;
|
lm.nodeId = node.id;
|
||||||
lm.depth = depth;
|
lm.depth = depth;
|
||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||||
lm.nodeKind = node.kind;
|
lm.nodeKind = node.kind;
|
||||||
lm.foldHead = true;
|
lm.foldHead = true;
|
||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
|
|||||||
@@ -145,6 +145,58 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
case EditTarget::Value:
|
case EditTarget::Value:
|
||||||
setNodeValue(nodeIdx, subLine, text);
|
setNodeValue(nodeIdx, subLine, text);
|
||||||
break;
|
break;
|
||||||
|
case EditTarget::BaseAddress: {
|
||||||
|
QString s = text.trimmed();
|
||||||
|
// Support simple equations: 0x10+0x4, 0x100-0x10, etc.
|
||||||
|
uint64_t newBase = 0;
|
||||||
|
bool ok = true;
|
||||||
|
int pos = 0;
|
||||||
|
bool firstTerm = true;
|
||||||
|
bool adding = true;
|
||||||
|
|
||||||
|
while (pos < s.size() && ok) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||||
|
if (pos >= s.size()) break;
|
||||||
|
|
||||||
|
// Check for +/- operator (except first term)
|
||||||
|
if (!firstTerm) {
|
||||||
|
if (s[pos] == '+') { adding = true; pos++; }
|
||||||
|
else if (s[pos] == '-') { adding = false; pos++; }
|
||||||
|
else { ok = false; break; }
|
||||||
|
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse hex number (with or without 0x prefix)
|
||||||
|
int start = pos;
|
||||||
|
bool hasPrefix = (pos + 1 < s.size() &&
|
||||||
|
s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'));
|
||||||
|
if (hasPrefix) pos += 2;
|
||||||
|
|
||||||
|
int numStart = pos;
|
||||||
|
while (pos < s.size() && (s[pos].isDigit() ||
|
||||||
|
(s[pos] >= 'a' && s[pos] <= 'f') ||
|
||||||
|
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
|
||||||
|
|
||||||
|
if (pos == numStart) { ok = false; break; }
|
||||||
|
|
||||||
|
QString numStr = s.mid(numStart, pos - numStart);
|
||||||
|
uint64_t val = numStr.toULongLong(&ok, 16);
|
||||||
|
if (!ok) break;
|
||||||
|
|
||||||
|
if (adding) newBase += val;
|
||||||
|
else newBase -= val;
|
||||||
|
|
||||||
|
firstTerm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok && newBase != m_doc->tree.baseAddress) {
|
||||||
|
uint64_t oldBase = m_doc->tree.baseAddress;
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::ChangeBase{oldBase, newBase}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
// Always refresh to restore canonical text (handles parse failures, no-ops, etc.)
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
45
src/core.h
45
src/core.h
@@ -427,6 +427,7 @@ struct LineMeta {
|
|||||||
bool foldHead = false;
|
bool foldHead = false;
|
||||||
bool foldCollapsed = false;
|
bool foldCollapsed = false;
|
||||||
bool isContinuation = false;
|
bool isContinuation = false;
|
||||||
|
bool isRootHeader = false; // true for top-level struct headers (base address editable)
|
||||||
LineKind lineKind = LineKind::Field;
|
LineKind lineKind = LineKind::Field;
|
||||||
NodeKind nodeKind = NodeKind::Int32;
|
NodeKind nodeKind = NodeKind::Int32;
|
||||||
QString offsetText;
|
QString offsetText;
|
||||||
@@ -468,15 +469,16 @@ struct ColumnSpan {
|
|||||||
bool valid = false;
|
bool valid = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class EditTarget { Name, Type, Value };
|
enum class EditTarget { Name, Type, Value, BaseAddress };
|
||||||
|
|
||||||
// Column layout constants (shared with format.cpp span computation)
|
// Column layout constants (shared with format.cpp span computation)
|
||||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||||
inline constexpr int kColType = 10;
|
inline constexpr int kColType = 10;
|
||||||
inline constexpr int kColName = 22;
|
inline constexpr int kColName = 22;
|
||||||
inline constexpr int kColValue = 32;
|
inline constexpr int kColValue = 32;
|
||||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||||
inline constexpr int kSepWidth = 2;
|
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||||
|
inline constexpr int kSepWidth = 2;
|
||||||
|
|
||||||
inline ColumnSpan typeSpanFor(const LineMeta& lm) {
|
inline ColumnSpan typeSpanFor(const LineMeta& lm) {
|
||||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||||
@@ -541,6 +543,33 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) {
|
|||||||
return {start, lineLength, start < lineLength};
|
return {start, lineLength, start < lineLength};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base address span (only valid for root struct headers)
|
||||||
|
// 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: "));
|
||||||
|
if (baseIdx < 0) return {};
|
||||||
|
int startPos = baseIdx + 6; // after "base: "
|
||||||
|
// Value goes to end of line
|
||||||
|
int endPos = lineText.size();
|
||||||
|
while (endPos > startPos && lineText[endPos-1].isSpace())
|
||||||
|
endPos--;
|
||||||
|
if (endPos <= startPos) return {};
|
||||||
|
return {startPos, endPos, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: "));
|
||||||
|
if (baseIdx < 0) return {};
|
||||||
|
int endPos = lineText.size();
|
||||||
|
while (endPos > baseIdx && lineText[endPos-1].isSpace())
|
||||||
|
endPos--;
|
||||||
|
return {baseIdx, endPos, true};
|
||||||
|
}
|
||||||
|
|
||||||
// ── ViewState ──
|
// ── ViewState ──
|
||||||
|
|
||||||
struct ViewState {
|
struct ViewState {
|
||||||
@@ -573,7 +602,9 @@ namespace fmt {
|
|||||||
const QString& comment = {});
|
const QString& comment = {});
|
||||||
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
|
QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation);
|
||||||
QString fmtStructHeader(const Node& node, int depth);
|
QString fmtStructHeader(const Node& node, int depth);
|
||||||
|
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress);
|
||||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||||
|
QString validateBaseAddress(const QString& text);
|
||||||
QString indent(int depth);
|
QString indent(int depth);
|
||||||
QString readValue(const Node& node, const Provider& prov,
|
QString readValue(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int subLine);
|
uint64_t addr, int subLine);
|
||||||
|
|||||||
230
src/editor.cpp
230
src/editor.cpp
@@ -21,8 +21,10 @@ static const QColor kBgMargin("#252526");
|
|||||||
static const QColor kFgMargin("#858585");
|
static const QColor kFgMargin("#858585");
|
||||||
static const QColor kFgMarginDim("#505050");
|
static const QColor kFgMarginDim("#505050");
|
||||||
|
|
||||||
static constexpr int IND_EDITABLE = 8;
|
static constexpr int IND_EDITABLE = 8;
|
||||||
static constexpr int IND_HEX_DIM = 9;
|
static constexpr int IND_HEX_DIM = 9;
|
||||||
|
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
||||||
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||||
|
|
||||||
static QString g_fontName = "Consolas";
|
static QString g_fontName = "Consolas";
|
||||||
|
|
||||||
@@ -85,6 +87,8 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
if (!m_editState.active) return;
|
if (!m_editState.active) return;
|
||||||
if (m_editState.target == EditTarget::Value)
|
if (m_editState.target == EditTarget::Value)
|
||||||
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
||||||
|
if (m_editState.target == EditTarget::Type)
|
||||||
|
QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_sci, &QsciScintilla::selectionChanged,
|
connect(m_sci, &QsciScintilla::selectionChanged,
|
||||||
@@ -129,6 +133,17 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_HEX_DIM, QColor("#505050"));
|
IND_HEX_DIM, QColor("#505050"));
|
||||||
|
|
||||||
|
// Base address indicator — green like comments
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_BASE_ADDR, QColor("#6a9955"));
|
||||||
|
|
||||||
|
// Hover span indicator — blue text like a link
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_HOVER_SPAN, QColor("#569cd6"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::setupLexer() {
|
void RcxEditor::setupLexer() {
|
||||||
@@ -277,6 +292,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
applyMarkers(result.meta);
|
applyMarkers(result.meta);
|
||||||
applyFoldLevels(result.meta);
|
applyFoldLevels(result.meta);
|
||||||
applyHexDimming(result.meta);
|
applyHexDimming(result.meta);
|
||||||
|
applyBaseAddressColoring(result.meta);
|
||||||
|
|
||||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
@@ -468,6 +484,23 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR);
|
||||||
|
for (int i = 0; i < meta.size(); i++) {
|
||||||
|
const LineMeta& lm = meta[i];
|
||||||
|
if (!lm.isRootHeader) continue;
|
||||||
|
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;
|
||||||
|
if (posB > posA)
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shared inline-edit shutdown ──
|
// ── Shared inline-edit shutdown ──
|
||||||
|
|
||||||
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
||||||
@@ -557,9 +590,10 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|||||||
|
|
||||||
ColumnSpan s;
|
ColumnSpan s;
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case EditTarget::Type: s = typeSpan(*lm); break;
|
case EditTarget::Type: s = typeSpan(*lm); break;
|
||||||
case EditTarget::Name: s = nameSpan(*lm); break;
|
case EditTarget::Name: s = nameSpan(*lm); break;
|
||||||
case EditTarget::Value: s = valueSpan(*lm, textLen); break;
|
case EditTarget::Value: s = valueSpan(*lm, textLen); break;
|
||||||
|
case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s.valid && t == EditTarget::Name)
|
if (!s.valid && t == EditTarget::Name)
|
||||||
@@ -623,6 +657,7 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
ColumnSpan ts = RcxEditor::typeSpan(lm);
|
ColumnSpan ts = RcxEditor::typeSpan(lm);
|
||||||
ColumnSpan ns = RcxEditor::nameSpan(lm);
|
ColumnSpan ns = RcxEditor::nameSpan(lm);
|
||||||
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen);
|
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen);
|
||||||
|
ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers
|
||||||
|
|
||||||
if (!ns.valid)
|
if (!ns.valid)
|
||||||
ns = headerNameSpan(lm, lineText);
|
ns = headerNameSpan(lm, lineText);
|
||||||
@@ -631,7 +666,8 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
return s.valid && col >= s.start && col < s.end;
|
return s.valid && col >= s.start && col < s.end;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inSpan(ts)) outTarget = EditTarget::Type;
|
if (inSpan(bs)) outTarget = EditTarget::BaseAddress;
|
||||||
|
else if (inSpan(ts)) outTarget = EditTarget::Type;
|
||||||
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
||||||
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
||||||
else return false;
|
else return false;
|
||||||
@@ -841,38 +877,22 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
|||||||
// ── Edit mode key handling ──
|
// ── Edit mode key handling ──
|
||||||
|
|
||||||
bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
||||||
bool autocActive = m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCACTIVE);
|
// User list is handled via userListActivated signal, not here
|
||||||
|
// SCI_AUTOCACTIVE is for autocomplete, not user lists
|
||||||
|
|
||||||
switch (ke->key()) {
|
switch (ke->key()) {
|
||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
case Qt::Key_Tab:
|
case Qt::Key_Tab:
|
||||||
if (autocActive && m_editState.target == EditTarget::Type) {
|
|
||||||
// Extract selected typeName directly from autocomplete
|
|
||||||
QByteArray buf(256, '\0');
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT,
|
|
||||||
(unsigned long)256, (void*)buf.data());
|
|
||||||
QString selectedType = QString::fromUtf8(buf.constData());
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
|
|
||||||
|
|
||||||
auto info = endInlineEdit();
|
|
||||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
commitInlineEdit();
|
commitInlineEdit();
|
||||||
return true;
|
return true;
|
||||||
case Qt::Key_Escape:
|
case Qt::Key_Escape:
|
||||||
if (autocActive) {
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL);
|
|
||||||
return true; // close popup, stay in edit mode
|
|
||||||
}
|
|
||||||
cancelInlineEdit();
|
cancelInlineEdit();
|
||||||
return true;
|
return true;
|
||||||
case Qt::Key_Up:
|
case Qt::Key_Up:
|
||||||
case Qt::Key_Down:
|
case Qt::Key_Down:
|
||||||
case Qt::Key_PageUp:
|
case Qt::Key_PageUp:
|
||||||
case Qt::Key_PageDown:
|
case Qt::Key_PageDown:
|
||||||
if (autocActive) return false; // let Scintilla navigate list
|
|
||||||
return true; // block line navigation
|
return true; // block line navigation
|
||||||
case Qt::Key_Delete:
|
case Qt::Key_Delete:
|
||||||
return true; // block to prevent eating trailing content
|
return true; // block to prevent eating trailing content
|
||||||
@@ -963,11 +983,17 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1,
|
||||||
QColor("#264f78"));
|
QColor("#264f78"));
|
||||||
|
|
||||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
// Use correct UTF-8 position conversion (not lineStart + col!)
|
||||||
(unsigned long)line);
|
m_editState.posStart = posFromCol(m_sci, line, norm.start);
|
||||||
long posStart = lineStart + m_editState.spanStart;
|
m_editState.posEnd = posFromCol(m_sci, line, norm.end);
|
||||||
long posEnd = posStart + trimmed.toUtf8().size();
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posStart, posEnd);
|
// For Value/BaseAddress: skip 0x prefix in selection (select only the number)
|
||||||
|
long selStart = m_editState.posStart;
|
||||||
|
if ((target == EditTarget::Value || target == EditTarget::BaseAddress) &&
|
||||||
|
trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
|
||||||
|
selStart = m_editState.posStart + 2; // Skip "0x"
|
||||||
|
}
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd);
|
||||||
|
|
||||||
// Show initial edit hint in comment column
|
// Show initial edit hint in comment column
|
||||||
if (target == EditTarget::Value)
|
if (target == EditTarget::Value)
|
||||||
@@ -998,34 +1024,31 @@ void RcxEditor::clampEditSelection() {
|
|||||||
int editEnd = editEndCol();
|
int editEnd = editEndCol();
|
||||||
bool isCursor = (selStartLine == selEndLine && selStartCol == selEndCol);
|
bool isCursor = (selStartLine == selEndLine && selStartCol == selEndCol);
|
||||||
|
|
||||||
|
// Don't fight cursor positioning - only clamp actual selections
|
||||||
if (isCursor) {
|
if (isCursor) {
|
||||||
// Cursor positioning (no selection) - only clamp if outside bounds
|
s_clamping = false;
|
||||||
if (selStartLine != m_editState.line ||
|
return;
|
||||||
selStartCol < m_editState.spanStart || selStartCol > editEnd) {
|
|
||||||
int clampedCol = qBound(m_editState.spanStart, selStartCol, editEnd);
|
|
||||||
m_sci->setCursorPosition(m_editState.line, clampedCol);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Actual selection - clamp both ends to edit span
|
|
||||||
bool clamped = false;
|
|
||||||
|
|
||||||
// Force to edit line
|
|
||||||
if (selStartLine != m_editState.line || selEndLine != m_editState.line) {
|
|
||||||
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
|
||||||
m_editState.line, editEnd);
|
|
||||||
s_clamping = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; }
|
|
||||||
if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; }
|
|
||||||
if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; }
|
|
||||||
if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; }
|
|
||||||
|
|
||||||
if (clamped)
|
|
||||||
m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actual selection - clamp both ends to edit span
|
||||||
|
bool clamped = false;
|
||||||
|
|
||||||
|
// Force to edit line
|
||||||
|
if (selStartLine != m_editState.line || selEndLine != m_editState.line) {
|
||||||
|
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
||||||
|
m_editState.line, editEnd);
|
||||||
|
s_clamping = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; }
|
||||||
|
if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; }
|
||||||
|
if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; }
|
||||||
|
if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; }
|
||||||
|
|
||||||
|
if (clamped)
|
||||||
|
m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol);
|
||||||
|
|
||||||
s_clamping = false;
|
s_clamping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,6 +1066,10 @@ void RcxEditor::commitInlineEdit() {
|
|||||||
if (editedLen > 0)
|
if (editedLen > 0)
|
||||||
editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed();
|
editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed();
|
||||||
|
|
||||||
|
// For Type edits: if nothing changed, commit original
|
||||||
|
if (m_editState.target == EditTarget::Type && editedText.isEmpty())
|
||||||
|
editedText = m_editState.original;
|
||||||
|
|
||||||
auto info = endInlineEdit();
|
auto info = endInlineEdit();
|
||||||
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText);
|
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText);
|
||||||
}
|
}
|
||||||
@@ -1056,27 +1083,63 @@ void RcxEditor::cancelInlineEdit() {
|
|||||||
emit inlineEditCancelled();
|
emit inlineEditCancelled();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Type autocomplete ──
|
// ── Type picker (user list) ──
|
||||||
|
|
||||||
void RcxEditor::showTypeAutocomplete() {
|
void RcxEditor::showTypeAutocomplete() {
|
||||||
|
// Replace original type with spaces (keeps layout, clears for typing)
|
||||||
|
int len = m_editState.original.size();
|
||||||
|
QString spaces(len, ' ');
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
|
||||||
|
m_editState.posStart, m_editState.posEnd);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
|
||||||
|
(uintptr_t)0, spaces.toUtf8().constData());
|
||||||
|
|
||||||
|
// Position cursor at start
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
|
||||||
|
|
||||||
|
showTypeListFiltered(QString()); // Show full list initially
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcxEditor::showTypeListFiltered(const QString& filter) {
|
||||||
if (!m_editState.active || m_editState.target != EditTarget::Type)
|
if (!m_editState.active || m_editState.target != EditTarget::Type)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Selection stays intact - typing/autocomplete will replace selected text
|
// Filter type names by prefix
|
||||||
|
QStringList all = allTypeNamesForUI();
|
||||||
|
QStringList filtered;
|
||||||
|
for (const QString& t : all) {
|
||||||
|
if (filter.isEmpty() || t.startsWith(filter, Qt::CaseInsensitive))
|
||||||
|
filtered << t;
|
||||||
|
}
|
||||||
|
if (filtered.isEmpty()) return; // No matches - keep list hidden
|
||||||
|
|
||||||
// Build list from typeName (matches what the editor displays)
|
// Show user list (id=1 for types) - selection handled by userListActivated signal
|
||||||
QByteArray list = allTypeNamesForUI().join(' ').toUtf8();
|
QByteArray list = filtered.join(' ').toUtf8();
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
|
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' ');
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1);
|
(uintptr_t)1, list.constData());
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW,
|
}
|
||||||
(uintptr_t)0, list.constData());
|
|
||||||
|
|
||||||
// Highlight the current type in the list
|
void RcxEditor::updateTypeListFilter() {
|
||||||
QByteArray cur = m_editState.original.toUtf8();
|
if (!m_editState.active || m_editState.target != EditTarget::Type)
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT,
|
return;
|
||||||
(uintptr_t)0, cur.constData());
|
|
||||||
|
|
||||||
|
// Get currently typed text from line
|
||||||
|
QString lineText = getLineText(m_sci, m_editState.line);
|
||||||
|
long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
|
||||||
|
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
|
(unsigned long)m_editState.line);
|
||||||
|
int col = (int)(curPos - lineStart);
|
||||||
|
|
||||||
|
// Extract text from spanStart to cursor
|
||||||
|
int len = col - m_editState.spanStart;
|
||||||
|
if (len <= 0) {
|
||||||
|
showTypeListFiltered(QString()); // Show full list
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString typed = lineText.mid(m_editState.spanStart, len);
|
||||||
|
showTypeListFiltered(typed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Editable-field text-color indicator ──
|
// ── Editable-field text-color indicator ──
|
||||||
@@ -1129,6 +1192,12 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
// ── Hover cursor ──
|
// ── Hover cursor ──
|
||||||
|
|
||||||
void RcxEditor::applyHoverCursor() {
|
void RcxEditor::applyHoverCursor() {
|
||||||
|
// Clear previous hover span indicator
|
||||||
|
if (m_hoverSpanLine >= 0) {
|
||||||
|
clearIndicatorLine(IND_HOVER_SPAN, m_hoverSpanLine);
|
||||||
|
m_hoverSpanLine = -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Edit mode handles its own cursor (I-beam)
|
// Edit mode handles its own cursor (I-beam)
|
||||||
if (m_editState.active)
|
if (m_editState.active)
|
||||||
return;
|
return;
|
||||||
@@ -1147,6 +1216,15 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
int line; EditTarget t;
|
int line; EditTarget t;
|
||||||
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
|
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
|
||||||
|
|
||||||
|
// Apply hover span indicator (blue text like a link)
|
||||||
|
if (tokenHit) {
|
||||||
|
NormalizedSpan span;
|
||||||
|
if (resolvedSpanFor(line, t, span)) {
|
||||||
|
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
|
||||||
|
m_hoverSpanLine = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Also show pointer cursor for fold column on fold-head lines
|
// Also show pointer cursor for fold column on fold-head lines
|
||||||
bool interactive = tokenHit;
|
bool interactive = tokenHit;
|
||||||
if (!interactive) {
|
if (!interactive) {
|
||||||
@@ -1175,18 +1253,24 @@ void RcxEditor::setEditComment(const QString& comment) {
|
|||||||
if (s_updating) return;
|
if (s_updating) return;
|
||||||
s_updating = true;
|
s_updating = true;
|
||||||
|
|
||||||
// Comment is always at end of line - calculate dynamically as value length changes
|
|
||||||
QString lineText = getLineText(m_sci, m_editState.line);
|
QString lineText = getLineText(m_sci, m_editState.line);
|
||||||
int startCol = lineText.size() - kColComment;
|
|
||||||
if (startCol < 0) { s_updating = false; return; }
|
|
||||||
|
|
||||||
QString padded = comment.leftJustified(kColComment, ' ').left(kColComment);
|
// Place comment 2 spaces after current value, prefixed with //
|
||||||
|
int valueEnd = editEndCol();
|
||||||
|
int startCol = valueEnd + 2; // 2 spaces after value
|
||||||
|
int endCol = lineText.size();
|
||||||
|
int availWidth = endCol - startCol;
|
||||||
|
if (availWidth <= 0) { s_updating = false; return; }
|
||||||
|
|
||||||
|
// Format as "//<comment>" (no space after //)
|
||||||
|
QString formatted = QStringLiteral("//") + comment;
|
||||||
|
QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth);
|
||||||
|
|
||||||
// Use direct position calculation from line start
|
// Use direct position calculation from line start
|
||||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
(unsigned long)m_editState.line);
|
(unsigned long)m_editState.line);
|
||||||
long posA = lineStart + startCol;
|
long posA = lineStart + startCol;
|
||||||
long posB = lineStart + startCol + kColComment;
|
long posB = lineStart + endCol;
|
||||||
|
|
||||||
QByteArray utf8 = padded.toUtf8();
|
QByteArray utf8 = padded.toUtf8();
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
||||||
@@ -1194,6 +1278,10 @@ void RcxEditor::setEditComment(const QString& comment) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
||||||
(uintptr_t)utf8.size(), utf8.constData());
|
(uintptr_t)utf8.size(), utf8.constData());
|
||||||
|
|
||||||
|
// Apply green color to hint text (reuse IND_BASE_ADDR which is green)
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA);
|
||||||
|
|
||||||
s_updating = false;
|
s_updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ private:
|
|||||||
bool m_cursorOverridden = false;
|
bool m_cursorOverridden = false;
|
||||||
uint64_t m_hoveredNodeId = 0;
|
uint64_t m_hoveredNodeId = 0;
|
||||||
QSet<uint64_t> m_currentSelIds;
|
QSet<uint64_t> m_currentSelIds;
|
||||||
|
int m_hoverSpanLine = -1; // Line with hover span indicator
|
||||||
// ── Drag selection ──
|
// ── Drag selection ──
|
||||||
bool m_dragging = false;
|
bool m_dragging = false;
|
||||||
bool m_dragStarted = false; // true once drag threshold exceeded
|
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||||
@@ -87,6 +88,8 @@ private:
|
|||||||
int spanStart = 0;
|
int spanStart = 0;
|
||||||
int linelenAfterReplace = 0;
|
int linelenAfterReplace = 0;
|
||||||
QString original;
|
QString original;
|
||||||
|
long posStart = 0; // Scintilla position of edit start
|
||||||
|
long posEnd = 0; // Scintilla position of edit end
|
||||||
NodeKind editKind = NodeKind::Int32;
|
NodeKind editKind = NodeKind::Int32;
|
||||||
int commentCol = -1; // fixed comment column (stored at edit start)
|
int commentCol = -1; // fixed comment column (stored at edit start)
|
||||||
bool lastValidationOk = true; // track state to avoid redundant updates
|
bool lastValidationOk = true; // track state to avoid redundant updates
|
||||||
@@ -104,12 +107,15 @@ private:
|
|||||||
void applyMarkers(const QVector<LineMeta>& meta);
|
void applyMarkers(const QVector<LineMeta>& meta);
|
||||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||||
|
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||||
|
|
||||||
void commitInlineEdit();
|
void commitInlineEdit();
|
||||||
int editEndCol() const;
|
int editEndCol() const;
|
||||||
bool handleNormalKey(QKeyEvent* ke);
|
bool handleNormalKey(QKeyEvent* ke);
|
||||||
bool handleEditKey(QKeyEvent* ke);
|
bool handleEditKey(QKeyEvent* ke);
|
||||||
void showTypeAutocomplete();
|
void showTypeAutocomplete();
|
||||||
|
void showTypeListFiltered(const QString& filter);
|
||||||
|
void updateTypeListFilter();
|
||||||
void paintEditableSpans(int line);
|
void paintEditableSpans(int line);
|
||||||
void updateEditableIndicators(int line);
|
void updateEditableIndicators(int line);
|
||||||
void applyHoverCursor();
|
void applyHoverCursor();
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ QString fmtStructHeader(const Node& node, int depth) {
|
|||||||
QStringLiteral(" ") + node.name + QStringLiteral(" {");
|
QStringLiteral(" ") + node.name + QStringLiteral(" {");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress) {
|
||||||
|
// Format: "struct Name { base: 0x00400000" - single space after {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
QString fmtStructFooter(const Node& node, int depth, int totalSize) {
|
QString fmtStructFooter(const Node& node, int depth, int totalSize) {
|
||||||
QString s = indent(depth) + QStringLiteral("};");
|
QString s = indent(depth) + QStringLiteral("};");
|
||||||
if (totalSize > 0)
|
if (totalSize > 0)
|
||||||
@@ -444,9 +452,48 @@ QString validateValue(NodeKind kind, const QString& text) {
|
|||||||
const auto* m = kindMeta(kind);
|
const auto* m = kindMeta(kind);
|
||||||
if (m && m->size > 0 && m->size <= 8) {
|
if (m && m->size > 0 && m->size <= 8) {
|
||||||
uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1);
|
uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1);
|
||||||
return QStringLiteral("max 0x%1").arg(maxVal, m->size * 2, 16, QChar('0'));
|
return QStringLiteral("too large! max=0x%1").arg(maxVal, m->size * 2, 16, QChar('0'));
|
||||||
}
|
}
|
||||||
return QStringLiteral("invalid");
|
return QStringLiteral("invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Base address validation (supports simple +/- equations) ──
|
||||||
|
|
||||||
|
QString validateBaseAddress(const QString& text) {
|
||||||
|
QString s = text.trimmed();
|
||||||
|
if (s.isEmpty()) return QStringLiteral("empty");
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
bool firstTerm = true;
|
||||||
|
|
||||||
|
while (pos < s.size()) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||||
|
if (pos >= s.size()) break;
|
||||||
|
|
||||||
|
// Check for +/- operator (except first term)
|
||||||
|
if (!firstTerm) {
|
||||||
|
if (s[pos] == '+' || s[pos] == '-') pos++;
|
||||||
|
else return QStringLiteral("invalid '%1'").arg(s[pos]);
|
||||||
|
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip 0x prefix if present
|
||||||
|
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
|
||||||
|
pos += 2;
|
||||||
|
|
||||||
|
// Must have at least one hex digit
|
||||||
|
int numStart = pos;
|
||||||
|
while (pos < s.size() && (s[pos].isDigit() ||
|
||||||
|
(s[pos] >= 'a' && s[pos] <= 'f') ||
|
||||||
|
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
|
||||||
|
|
||||||
|
if (pos == numStart) return QStringLiteral("invalid");
|
||||||
|
|
||||||
|
firstTerm = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace rcx::fmt
|
} // namespace rcx::fmt
|
||||||
|
|||||||
@@ -455,87 +455,99 @@ private slots:
|
|||||||
QVERIFY(sel.contains(lm->nodeIdx));
|
QVERIFY(sel.contains(lm->nodeIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: value edit echoes to comment column ──
|
// ── Test: base address changes affect header display ──
|
||||||
void testValueEditCommentEcho() {
|
void testBaseAddressDisplay() {
|
||||||
m_editor->applyDocument(m_result);
|
// Create tree with base address 0x10
|
||||||
|
NodeTree tree = makeTestTree();
|
||||||
|
tree.baseAddress = 0x10;
|
||||||
|
FileProvider prov = makeTestProvider();
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
// Begin value edit on line 1 (UInt16 field)
|
m_editor->applyDocument(result);
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
|
|
||||||
QVERIFY(ok);
|
|
||||||
QVERIFY(m_editor->isEditing());
|
|
||||||
|
|
||||||
// Get the line text before any typing
|
// Line 0 should be the struct header with isRootHeader=true
|
||||||
QString lineBefore;
|
const LineMeta* lm = m_editor->metaForLine(0);
|
||||||
int len = (int)m_editor->scintilla()->SendScintilla(
|
QVERIFY(lm);
|
||||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
|
QCOMPARE(lm->lineKind, LineKind::Header);
|
||||||
if (len > 0) {
|
QVERIFY(lm->isRootHeader);
|
||||||
QByteArray buf(len + 1, '\0');
|
|
||||||
m_editor->scintilla()->SendScintilla(
|
|
||||||
QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data());
|
|
||||||
lineBefore = QString::fromUtf8(buf.constData(), len).trimmed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial comment should contain "Enter=Save Esc=Cancel"
|
// Get header line text - should contain "0x10"
|
||||||
QVERIFY2(lineBefore.contains("Enter=Save"),
|
|
||||||
qPrintable("Initial comment missing, got: " + lineBefore));
|
|
||||||
|
|
||||||
// Type a digit to trigger validateEditLive
|
|
||||||
QKeyEvent key5(QEvent::KeyPress, Qt::Key_5, Qt::NoModifier, "5");
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &key5);
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// Get line text after typing
|
|
||||||
QString lineAfter;
|
|
||||||
len = (int)m_editor->scintilla()->SendScintilla(
|
|
||||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
|
|
||||||
if (len > 0) {
|
|
||||||
QByteArray buf(len + 1, '\0');
|
|
||||||
m_editor->scintilla()->SendScintilla(
|
|
||||||
QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data());
|
|
||||||
lineAfter = QString::fromUtf8(buf.constData(), len).trimmed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comment should show "!" prefix for invalid value
|
|
||||||
// Since "0x5a4d" + "5" = "0x5a4d5" = 370509 > 65535, it's invalid for UInt16
|
|
||||||
QVERIFY2(lineAfter.contains("! "),
|
|
||||||
qPrintable("Comment should show '!' for invalid value, got: " + lineAfter));
|
|
||||||
|
|
||||||
// Cancel and reset
|
|
||||||
m_editor->cancelInlineEdit();
|
|
||||||
m_editor->applyDocument(m_result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Test: value validation shows error indicator ──
|
|
||||||
void testValueValidationError() {
|
|
||||||
m_editor->applyDocument(m_result);
|
|
||||||
|
|
||||||
// Begin value edit on line 1 (UInt16 field, value = 23117)
|
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1);
|
|
||||||
QVERIFY(ok);
|
|
||||||
|
|
||||||
// Type "999" to make value invalid for UInt16 (appends to existing, making it too large)
|
|
||||||
// Original value 23117 -> typing "999" at end makes it invalid (23117999 > 65535)
|
|
||||||
const char* digits = "999";
|
|
||||||
for (int i = 0; digits[i]; i++) {
|
|
||||||
QKeyEvent key(QEvent::KeyPress, Qt::Key_9, Qt::NoModifier, QString(digits[i]));
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &key);
|
|
||||||
QApplication::processEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get line text - comment should show "! " prefix (error)
|
|
||||||
QString lineText;
|
QString lineText;
|
||||||
int len = (int)m_editor->scintilla()->SendScintilla(
|
int len = (int)m_editor->scintilla()->SendScintilla(
|
||||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1);
|
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
QByteArray buf(len + 1, '\0');
|
QByteArray buf(len + 1, '\0');
|
||||||
m_editor->scintilla()->SendScintilla(
|
m_editor->scintilla()->SendScintilla(
|
||||||
QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data());
|
QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data());
|
||||||
lineText = QString::fromUtf8(buf.constData(), len).trimmed();
|
lineText = QString::fromUtf8(buf.constData(), len).trimmed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comment should show "! " prefix for invalid value
|
// Verify base address appears in header
|
||||||
QVERIFY2(lineText.contains("! "),
|
QVERIFY2(lineText.contains("0x10") || lineText.contains("0X10"),
|
||||||
qPrintable("Comment should show '! ' for invalid value, got: " + lineText));
|
qPrintable("Header should contain base address 0x10, got: " + lineText));
|
||||||
|
|
||||||
|
// Verify struct keyword is present
|
||||||
|
QVERIFY2(lineText.contains("struct"),
|
||||||
|
qPrintable("Header should contain 'struct', got: " + lineText));
|
||||||
|
|
||||||
|
// Reset to original result
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: base address span is valid for root headers ──
|
||||||
|
void testBaseAddressSpan() {
|
||||||
|
NodeTree tree = makeTestTree();
|
||||||
|
tree.baseAddress = 0x140000000; // Large address to test span width
|
||||||
|
FileProvider prov = makeTestProvider();
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
|
m_editor->applyDocument(result);
|
||||||
|
|
||||||
|
// Line 0 should be root header
|
||||||
|
const LineMeta* lm = m_editor->metaForLine(0);
|
||||||
|
QVERIFY(lm);
|
||||||
|
QVERIFY(lm->isRootHeader);
|
||||||
|
|
||||||
|
// Get line text for span calculation
|
||||||
|
QString lineText;
|
||||||
|
int len = (int)m_editor->scintilla()->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||||
|
if (len > 0) {
|
||||||
|
QByteArray buf(len + 1, '\0');
|
||||||
|
m_editor->scintilla()->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data());
|
||||||
|
lineText = QString::fromUtf8(buf.constData(), len);
|
||||||
|
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
|
||||||
|
lineText.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base address span should be valid
|
||||||
|
ColumnSpan bs = baseAddressSpanFor(*lm, lineText);
|
||||||
|
QVERIFY2(bs.valid, "Base address span should be valid for root header");
|
||||||
|
QVERIFY(bs.start < bs.end);
|
||||||
|
|
||||||
|
// The span should cover the hex address
|
||||||
|
QString spanText = lineText.mid(bs.start, bs.end - bs.start);
|
||||||
|
QVERIFY2(spanText.contains("0x") || spanText.startsWith("0X"),
|
||||||
|
qPrintable("Span should contain hex address, got: " + spanText));
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test: base address edit begins on root header ──
|
||||||
|
void testBaseAddressEditBegins() {
|
||||||
|
NodeTree tree = makeTestTree();
|
||||||
|
tree.baseAddress = 0x10;
|
||||||
|
FileProvider prov = makeTestProvider();
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
|
m_editor->applyDocument(result);
|
||||||
|
|
||||||
|
// Begin base address edit on line 0 (root header)
|
||||||
|
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||||
|
QVERIFY2(ok, "Should be able to begin base address edit on root header");
|
||||||
|
QVERIFY(m_editor->isEditing());
|
||||||
|
|
||||||
// Cancel and reset
|
// Cancel and reset
|
||||||
m_editor->cancelInlineEdit();
|
m_editor->cancelInlineEdit();
|
||||||
|
|||||||
Reference in New Issue
Block a user