mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Fix mouse event sync: hover on click, drag threshold, indicator/selection alignment
This commit is contained in:
@@ -155,7 +155,6 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
|
|
||||||
void RcxController::refresh() {
|
void RcxController::refresh() {
|
||||||
m_lastResult = m_doc->compose();
|
m_lastResult = m_doc->compose();
|
||||||
qDebug() << "refresh() called, text length:" << m_lastResult.text.size();
|
|
||||||
|
|
||||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||||
QSet<uint64_t> valid;
|
QSet<uint64_t> valid;
|
||||||
@@ -265,15 +264,36 @@ void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, con
|
|||||||
|
|
||||||
void RcxController::removeNode(int nodeIdx) {
|
void RcxController::removeNode(int nodeIdx) {
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
|
uint64_t nodeId = node.id;
|
||||||
|
uint64_t parentId = node.parentId;
|
||||||
|
|
||||||
|
// Compute size of deleted node/subtree
|
||||||
|
int deletedSize = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
|
||||||
|
? m_doc->tree.structSpan(node.id) : node.byteSize();
|
||||||
|
int deletedEnd = node.offset + deletedSize;
|
||||||
|
|
||||||
|
// Find siblings after this node and compute offset adjustments
|
||||||
|
QVector<cmd::OffsetAdj> adjs;
|
||||||
|
if (parentId != 0) { // only adjust if not root-level
|
||||||
|
auto siblings = m_doc->tree.childrenOf(parentId);
|
||||||
|
for (int si : siblings) {
|
||||||
|
if (si == nodeIdx) continue;
|
||||||
|
auto& sib = m_doc->tree.nodes[si];
|
||||||
|
if (sib.offset >= deletedEnd) {
|
||||||
|
adjs.append({sib.id, sib.offset, sib.offset - deletedSize});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect subtree
|
||||||
QVector<int> indices = m_doc->tree.subtreeIndices(nodeId);
|
QVector<int> indices = m_doc->tree.subtreeIndices(nodeId);
|
||||||
QVector<Node> subtree;
|
QVector<Node> subtree;
|
||||||
for (int i : indices)
|
for (int i : indices)
|
||||||
subtree.append(m_doc->tree.nodes[i]);
|
subtree.append(m_doc->tree.nodes[i]);
|
||||||
|
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::Remove{nodeId, subtree}));
|
cmd::Remove{nodeId, subtree, adjs}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::toggleCollapse(int nodeIdx) {
|
void RcxController::toggleCollapse(int nodeIdx) {
|
||||||
@@ -316,13 +336,23 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
tree.addNode(c.node);
|
tree.addNode(c.node);
|
||||||
}
|
}
|
||||||
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
||||||
qDebug() << "applyCommand Remove, isUndo:" << isUndo << "nodeId:" << c.nodeId;
|
|
||||||
if (isUndo) {
|
if (isUndo) {
|
||||||
|
// Restore nodes first
|
||||||
for (const Node& n : c.subtree)
|
for (const Node& n : c.subtree)
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
|
// Revert offset adjustments
|
||||||
|
for (const auto& adj : c.offAdjs) {
|
||||||
|
int ai = tree.indexOfId(adj.nodeId);
|
||||||
|
if (ai >= 0) tree.nodes[ai].offset = adj.oldOffset;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Apply offset adjustments first (before removing changes indices)
|
||||||
|
for (const auto& adj : c.offAdjs) {
|
||||||
|
int ai = tree.indexOfId(adj.nodeId);
|
||||||
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||||
|
}
|
||||||
|
// Remove nodes
|
||||||
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
||||||
qDebug() << " Removing" << indices.size() << "nodes";
|
|
||||||
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
||||||
for (int idx : indices)
|
for (int idx : indices)
|
||||||
tree.nodes.remove(idx);
|
tree.nodes.remove(idx);
|
||||||
@@ -518,6 +548,11 @@ void RcxController::batchRemoveNodes(const QVector<int>& nodeIndices) {
|
|||||||
}
|
}
|
||||||
idSet = m_doc->tree.normalizePreferAncestors(idSet);
|
idSet = m_doc->tree.normalizePreferAncestors(idSet);
|
||||||
if (idSet.isEmpty()) return;
|
if (idSet.isEmpty()) return;
|
||||||
|
|
||||||
|
// Clear selection before delete (prevents stale highlight on shifted lines)
|
||||||
|
m_selIds.clear();
|
||||||
|
m_anchorLine = -1;
|
||||||
|
|
||||||
m_doc->undoStack.beginMacro(QString("Delete %1 nodes").arg(idSet.size()));
|
m_doc->undoStack.beginMacro(QString("Delete %1 nodes").arg(idSet.size()));
|
||||||
for (uint64_t id : idSet) {
|
for (uint64_t id : idSet) {
|
||||||
int idx = m_doc->tree.indexOfId(id);
|
int idx = m_doc->tree.indexOfId(id);
|
||||||
|
|||||||
45
src/core.h
45
src/core.h
@@ -449,7 +449,8 @@ namespace cmd {
|
|||||||
struct Rename { uint64_t nodeId; QString oldName, newName; };
|
struct Rename { uint64_t nodeId; QString oldName, newName; };
|
||||||
struct Collapse { uint64_t nodeId; bool oldState, newState; };
|
struct Collapse { uint64_t nodeId; bool oldState, newState; };
|
||||||
struct Insert { Node node; };
|
struct Insert { Node node; };
|
||||||
struct Remove { uint64_t nodeId; QVector<Node> subtree; };
|
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||||
|
QVector<OffsetAdj> offAdjs; };
|
||||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||||
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
||||||
}
|
}
|
||||||
@@ -470,10 +471,12 @@ struct ColumnSpan {
|
|||||||
enum class EditTarget { Name, Type, Value };
|
enum class EditTarget { Name, Type, Value };
|
||||||
|
|
||||||
// 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 = 24;
|
inline constexpr int kColName = 24;
|
||||||
inline constexpr int kSepWidth = 2;
|
inline constexpr int kColValue = 22;
|
||||||
|
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||||
|
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 {};
|
||||||
@@ -494,25 +497,47 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm) {
|
|||||||
return {start, start + kColName, true};
|
return {start, start + kColName, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ColumnSpan valueSpanFor(const LineMeta& lm, int lineLength) {
|
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/) {
|
||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes...]
|
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||||
|
int valWidth = isHexPad ? 23 : kColValue; // hex bytes or value column
|
||||||
|
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation) {
|
||||||
int prefixW = isHexPad
|
int prefixW = isHexPad
|
||||||
? (kColType + kSepWidth + 8 + kSepWidth)
|
? (kColType + kSepWidth + 8 + kSepWidth)
|
||||||
: (kColType + kColName + 4);
|
: (kColType + kColName + 4);
|
||||||
int start = ind + prefixW;
|
int start = ind + prefixW;
|
||||||
return {start, lineLength, start < lineLength};
|
return {start, start + valWidth, true};
|
||||||
}
|
}
|
||||||
if (lm.lineKind != LineKind::Field) return {};
|
if (lm.lineKind != LineKind::Field) return {};
|
||||||
|
|
||||||
int start = isHexPad
|
int start = isHexPad
|
||||||
? (ind + kColType + kSepWidth + 8 + kSepWidth)
|
? (ind + kColType + kSepWidth + 8 + kSepWidth)
|
||||||
: (ind + kColType + kSepWidth + kColName + kSepWidth);
|
: (ind + kColType + kSepWidth + kColName + kSepWidth);
|
||||||
|
return {start, start + valWidth, true};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) {
|
||||||
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
|
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||||
|
int valWidth = isHexPad ? 23 : kColValue;
|
||||||
|
|
||||||
|
int start;
|
||||||
|
if (lm.isContinuation) {
|
||||||
|
int prefixW = isHexPad
|
||||||
|
? (kColType + kSepWidth + 8 + kSepWidth)
|
||||||
|
: (kColType + kColName + 4);
|
||||||
|
start = ind + prefixW + valWidth;
|
||||||
|
} else {
|
||||||
|
start = isHexPad
|
||||||
|
? (ind + kColType + kSepWidth + 8 + kSepWidth + valWidth)
|
||||||
|
: (ind + kColType + kSepWidth + kColName + kSepWidth + valWidth);
|
||||||
|
}
|
||||||
return {start, lineLength, start < lineLength};
|
return {start, lineLength, start < lineLength};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +569,8 @@ namespace fmt {
|
|||||||
QString fmtPointer32(uint32_t v);
|
QString fmtPointer32(uint32_t v);
|
||||||
QString fmtPointer64(uint64_t v);
|
QString fmtPointer64(uint64_t v);
|
||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine = 0);
|
uint64_t addr, int depth, int subLine = 0,
|
||||||
|
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 fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||||
@@ -555,6 +581,7 @@ namespace fmt {
|
|||||||
uint64_t addr, int subLine);
|
uint64_t addr, int subLine);
|
||||||
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
||||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||||
|
QString validateValue(NodeKind kind, const QString& text);
|
||||||
} // namespace fmt
|
} // namespace fmt
|
||||||
|
|
||||||
// ── Compose function forward declaration ──
|
// ── Compose function forward declaration ──
|
||||||
|
|||||||
232
src/editor.cpp
232
src/editor.cpp
@@ -22,7 +22,6 @@ 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_HOVER_TOK = 10;
|
|
||||||
|
|
||||||
static QString g_fontName = "Consolas";
|
static QString g_fontName = "Consolas";
|
||||||
|
|
||||||
@@ -85,7 +84,6 @@ 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)
|
||||||
validateEditLive();
|
validateEditLive();
|
||||||
updateEditTokenBox();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,15 +127,6 @@ 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"));
|
||||||
|
|
||||||
// Hovered editable token highlight (subtle background tint, no outline)
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
|
||||||
IND_HOVER_TOK, 8 /*INDIC_STRAIGHTBOX*/);
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
||||||
IND_HOVER_TOK, QColor("#569cd6"));
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
|
|
||||||
IND_HOVER_TOK, (long)35);
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETOUTLINEALPHA,
|
|
||||||
IND_HOVER_TOK, (long)0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::setupLexer() {
|
void RcxEditor::setupLexer() {
|
||||||
@@ -226,14 +215,14 @@ void RcxEditor::setupMarkers() {
|
|||||||
m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE);
|
m_sci->setMarkerBackgroundColor(QColor("#e5a00d"), M_CYCLE);
|
||||||
m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE);
|
m_sci->setMarkerForegroundColor(QColor("#e5a00d"), M_CYCLE);
|
||||||
|
|
||||||
// M_ERR (4): background (dark red)
|
// M_ERR (4): background (dark red - brightened for visibility)
|
||||||
m_sci->markerDefine(QsciScintilla::Background, M_ERR);
|
m_sci->markerDefine(QsciScintilla::Background, M_ERR);
|
||||||
m_sci->setMarkerBackgroundColor(QColor("#5c2020"), M_ERR);
|
m_sci->setMarkerBackgroundColor(QColor("#7a2e2e"), M_ERR);
|
||||||
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
m_sci->setMarkerForegroundColor(QColor("#ffffff"), M_ERR);
|
||||||
|
|
||||||
// M_STRUCT_BG (5): background tint for struct header/footer
|
// M_STRUCT_BG (5): struct header/footer (matches regular bg, may remove later)
|
||||||
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG);
|
m_sci->markerDefine(QsciScintilla::Background, M_STRUCT_BG);
|
||||||
m_sci->setMarkerBackgroundColor(QColor("#1a2638"), M_STRUCT_BG);
|
m_sci->setMarkerBackgroundColor(QColor("#1e1e1e"), M_STRUCT_BG);
|
||||||
m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG);
|
m_sci->setMarkerForegroundColor(QColor("#d4d4d4"), M_STRUCT_BG);
|
||||||
|
|
||||||
// M_HOVER (6): full-row hover highlight
|
// M_HOVER (6): full-row hover highlight
|
||||||
@@ -257,6 +246,7 @@ void RcxEditor::allocateMarginStyles() {
|
|||||||
QByteArray fontName = editorFont().family().toUtf8();
|
QByteArray fontName = editorFont().family().toUtf8();
|
||||||
int fontSize = editorFont().pointSize();
|
int fontSize = editorFont().pointSize();
|
||||||
|
|
||||||
|
// Margin styles (dim gray text)
|
||||||
for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) {
|
for (int s = MSTYLE_NORMAL; s <= MSTYLE_CONT; s++) {
|
||||||
unsigned long abs = (unsigned long)(base + s);
|
unsigned long abs = (unsigned long)(base + s);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE, abs, (long)0x505050);
|
||||||
@@ -286,11 +276,8 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
applyFoldLevels(result.meta);
|
applyFoldLevels(result.meta);
|
||||||
applyHexDimming(result.meta);
|
applyHexDimming(result.meta);
|
||||||
|
|
||||||
// Re-apply editable indicators for current cursor line
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
int line, col;
|
|
||||||
m_sci->getCursorPosition(&line, &col);
|
|
||||||
updateEditableIndicators(line);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||||
@@ -376,7 +363,7 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
m_currentSelIds = selIds;
|
m_currentSelIds = selIds;
|
||||||
m_sci->markerDeleteAll(M_SELECTED);
|
m_sci->markerDeleteAll(M_SELECTED);
|
||||||
|
|
||||||
// Clear all editable indicators, then repaint for selected + cursor line
|
// Clear all editable indicators, then repaint for selected lines only
|
||||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
||||||
@@ -388,13 +375,9 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also paint cursor line (even if not selected)
|
// Reset hint line - updateEditableIndicators will handle cursor hints
|
||||||
if (!m_editState.active) {
|
// on actual user navigation (not stale restored positions)
|
||||||
int curLine, col;
|
m_hintLine = -1;
|
||||||
m_sci->getCursorPosition(&curLine, &col);
|
|
||||||
paintEditableSpans(curLine);
|
|
||||||
m_hintLine = curLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
}
|
}
|
||||||
@@ -486,19 +469,22 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
|||||||
// ── Shared inline-edit shutdown ──
|
// ── Shared inline-edit shutdown ──
|
||||||
|
|
||||||
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
RcxEditor::EndEditInfo RcxEditor::endInlineEdit() {
|
||||||
// Clear edit token box and reset indicator color
|
// Clear edit comment and error marker before deactivating
|
||||||
clearIndicatorLine(IND_HOVER_TOK, m_editState.line);
|
if (m_editState.target == EditTarget::Value) {
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
setEditComment({}); // Clear to spaces
|
||||||
IND_HOVER_TOK, QColor("#569cd6"));
|
m_sci->markerDelete(m_editState.line, M_ERR);
|
||||||
|
}
|
||||||
EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target};
|
EndEditInfo info{m_editState.nodeIdx, m_editState.subLine, m_editState.target};
|
||||||
m_editState.active = false;
|
m_editState.active = false;
|
||||||
m_sci->setReadOnly(true);
|
m_sci->setReadOnly(true);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 0);
|
||||||
|
// Switch from I-beam to Arrow (keep override active to block Scintilla's cursor)
|
||||||
if (m_cursorOverridden) {
|
if (m_cursorOverridden) {
|
||||||
QApplication::restoreOverrideCursor();
|
QApplication::changeOverrideCursor(Qt::ArrowCursor);
|
||||||
m_cursorOverridden = false;
|
} else {
|
||||||
|
QApplication::setOverrideCursor(Qt::ArrowCursor);
|
||||||
|
m_cursorOverridden = true;
|
||||||
}
|
}
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
|
||||||
// Disable selection rendering again
|
// Disable selection rendering again
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
||||||
@@ -586,16 +572,28 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|||||||
|
|
||||||
RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
|
RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
|
||||||
HitInfo h;
|
HitInfo h;
|
||||||
|
|
||||||
|
// Try precise position first (works when cursor is over actual text)
|
||||||
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
|
||||||
(unsigned long)vp.x(), (long)vp.y());
|
(unsigned long)vp.x(), (long)vp.y());
|
||||||
if (pos < 0) return h;
|
if (pos >= 0) {
|
||||||
h.line = (int)m_sci->SendScintilla(
|
h.line = (int)m_sci->SendScintilla(
|
||||||
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
|
QsciScintillaBase::SCI_LINEFROMPOSITION, (unsigned long)pos);
|
||||||
h.col = (int)m_sci->SendScintilla(
|
h.col = (int)m_sci->SendScintilla(
|
||||||
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
|
QsciScintillaBase::SCI_GETCOLUMN, (unsigned long)pos);
|
||||||
|
} else {
|
||||||
|
// Fallback: calculate line from Y coordinate (for empty space past text)
|
||||||
|
int firstVisible = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
|
||||||
|
int lineHeight = (int)m_sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_TEXTHEIGHT, 0);
|
||||||
|
if (lineHeight > 0)
|
||||||
|
h.line = firstVisible + vp.y() / lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
if (h.line >= 0 && h.line < m_meta.size()) {
|
if (h.line >= 0 && h.line < m_meta.size()) {
|
||||||
h.nodeId = m_meta[h.line].nodeId;
|
h.nodeId = m_meta[h.line].nodeId;
|
||||||
h.inFoldCol = (h.col < kFoldCol && m_meta[h.line].foldHead);
|
h.inFoldCol = (h.col >= 0 && h.col < kFoldCol && m_meta[h.line].foldHead);
|
||||||
}
|
}
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -668,7 +666,16 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
&& event->type() == QEvent::MouseButtonPress) {
|
&& event->type() == QEvent::MouseButtonPress) {
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
if (me->button() == Qt::LeftButton) {
|
if (me->button() == Qt::LeftButton) {
|
||||||
|
// Sync hover to click position (prevents hover/selection desync)
|
||||||
|
m_lastHoverPos = me->pos();
|
||||||
|
m_hoverInside = true;
|
||||||
auto h = hitTest(me->pos());
|
auto h = hitTest(me->pos());
|
||||||
|
uint64_t newHoverId = (h.line >= 0) ? h.nodeId : 0;
|
||||||
|
if (newHoverId != m_hoveredNodeId) {
|
||||||
|
m_hoveredNodeId = newHoverId;
|
||||||
|
applyHoverHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
if (h.inFoldCol) {
|
if (h.inFoldCol) {
|
||||||
emit marginClicked(0, h.line, me->modifiers());
|
emit marginClicked(0, h.line, me->modifiers());
|
||||||
return true;
|
return true;
|
||||||
@@ -687,6 +694,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_dragging = true;
|
m_dragging = true;
|
||||||
|
m_dragStarted = false; // require threshold before extending
|
||||||
|
m_dragStartPos = me->pos();
|
||||||
m_dragLastLine = h.line;
|
m_dragLastLine = h.line;
|
||||||
m_dragInitMods = me->modifiers();
|
m_dragInitMods = me->modifiers();
|
||||||
|
|
||||||
@@ -705,10 +714,19 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Drag-select: extend selection as mouse moves with button held
|
// Drag-select: extend selection as mouse moves with button held
|
||||||
|
// Requires minimum drag distance to prevent accidental micro-drag selection
|
||||||
if (obj == m_sci->viewport() && !m_editState.active
|
if (obj == m_sci->viewport() && !m_editState.active
|
||||||
&& event->type() == QEvent::MouseMove && m_dragging) {
|
&& event->type() == QEvent::MouseMove && m_dragging) {
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
if (me->buttons() & Qt::LeftButton) {
|
if (me->buttons() & Qt::LeftButton) {
|
||||||
|
// Check drag threshold (8 pixels) before starting drag-selection
|
||||||
|
if (!m_dragStarted) {
|
||||||
|
int dy = me->pos().y() - m_dragStartPos.y();
|
||||||
|
if (qAbs(dy) < 8)
|
||||||
|
return false; // not yet a drag, let Scintilla handle
|
||||||
|
m_dragStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Flush deferred click before extending drag
|
// Flush deferred click before extending drag
|
||||||
if (m_pendingClickNodeId != 0) {
|
if (m_pendingClickNodeId != 0) {
|
||||||
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
||||||
@@ -722,16 +740,23 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
|
m_dragStarted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) {
|
if (obj == m_sci->viewport() && event->type() == QEvent::MouseButtonRelease) {
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
|
m_dragStarted = false;
|
||||||
if (m_pendingClickNodeId != 0) {
|
if (m_pendingClickNodeId != 0) {
|
||||||
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
emit nodeClicked(m_pendingClickLine, m_pendingClickNodeId,
|
||||||
m_pendingClickMods);
|
m_pendingClickMods);
|
||||||
m_pendingClickNodeId = 0;
|
m_pendingClickNodeId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Block double/triple-click during edit mode (prevents word/line selection)
|
||||||
|
if (obj == m_sci->viewport() && m_editState.active
|
||||||
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (obj == m_sci->viewport() && !m_editState.active
|
if (obj == m_sci->viewport() && !m_editState.active
|
||||||
&& event->type() == QEvent::MouseButtonDblClick) {
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
@@ -875,15 +900,8 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
|
|||||||
|
|
||||||
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
||||||
if (m_editState.active) return false;
|
if (m_editState.active) return false;
|
||||||
if (m_cursorOverridden) {
|
|
||||||
QApplication::restoreOverrideCursor();
|
|
||||||
m_cursorOverridden = false;
|
|
||||||
}
|
|
||||||
m_hoveredNodeId = 0;
|
m_hoveredNodeId = 0;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
// Clear hover token box (will be repainted as edit token box below)
|
|
||||||
clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine);
|
|
||||||
m_hoverTokLine = -1;
|
|
||||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
@@ -919,8 +937,13 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1);
|
||||||
m_sci->setReadOnly(false);
|
m_sci->setReadOnly(false);
|
||||||
QApplication::setOverrideCursor(Qt::IBeamCursor);
|
// Switch to I-beam for editing
|
||||||
m_cursorOverridden = true;
|
if (m_cursorOverridden) {
|
||||||
|
QApplication::changeOverrideCursor(Qt::IBeamCursor);
|
||||||
|
} else {
|
||||||
|
QApplication::setOverrideCursor(Qt::IBeamCursor);
|
||||||
|
m_cursorOverridden = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-enable selection rendering for inline edit
|
// Re-enable selection rendering for inline edit
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||||
@@ -932,7 +955,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
long posStart = lineStart + m_editState.spanStart;
|
long posStart = lineStart + m_editState.spanStart;
|
||||||
long posEnd = posStart + trimmed.toUtf8().size();
|
long posEnd = posStart + trimmed.toUtf8().size();
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd);
|
||||||
updateEditTokenBox();
|
|
||||||
|
// Show initial edit hint in comment column
|
||||||
|
if (target == EditTarget::Value)
|
||||||
|
setEditComment(QStringLiteral("// Enter=Save Esc=Cancel"));
|
||||||
|
|
||||||
if (target == EditTarget::Type)
|
if (target == EditTarget::Type)
|
||||||
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
|
QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete);
|
||||||
@@ -940,15 +966,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::updateEditTokenBox() {
|
|
||||||
clearIndicatorLine(IND_HOVER_TOK, m_editState.line);
|
|
||||||
|
|
||||||
int endCol = editEndCol();
|
|
||||||
if (endCol <= m_editState.spanStart) return;
|
|
||||||
|
|
||||||
fillIndicatorCols(IND_HOVER_TOK, m_editState.line, m_editState.spanStart, endCol);
|
|
||||||
}
|
|
||||||
|
|
||||||
int RcxEditor::editEndCol() const {
|
int RcxEditor::editEndCol() const {
|
||||||
QString lineText = getLineText(m_sci, m_editState.line);
|
QString lineText = getLineText(m_sci, m_editState.line);
|
||||||
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
||||||
@@ -1023,6 +1040,19 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
if (m_editState.active) return;
|
if (m_editState.active) return;
|
||||||
if (line == m_hintLine) return;
|
if (line == m_hintLine) return;
|
||||||
|
|
||||||
|
// If new line is selected, its indicators are managed by applySelectionOverlay
|
||||||
|
// But we still need to clear the old non-selected hint line
|
||||||
|
const LineMeta* newLm = metaForLine(line);
|
||||||
|
if (newLm && m_currentSelIds.contains(newLm->nodeId)) {
|
||||||
|
if (m_hintLine >= 0) {
|
||||||
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
||||||
|
if (!oldLm || !m_currentSelIds.contains(oldLm->nodeId))
|
||||||
|
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||||
|
}
|
||||||
|
m_hintLine = line;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clear old cursor line (only if not a selected node)
|
// Clear old cursor line (only if not a selected node)
|
||||||
if (m_hintLine >= 0) {
|
if (m_hintLine >= 0) {
|
||||||
const LineMeta* oldLm = metaForLine(m_hintLine);
|
const LineMeta* oldLm = metaForLine(m_hintLine);
|
||||||
@@ -1034,20 +1064,20 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
paintEditableSpans(line);
|
paintEditableSpans(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hover cursor (coalesced) ──
|
// ── Hover cursor ──
|
||||||
|
|
||||||
void RcxEditor::applyHoverCursor() {
|
void RcxEditor::applyHoverCursor() {
|
||||||
auto clearHoverTok = [&]() {
|
// Edit mode handles its own cursor (I-beam)
|
||||||
clearIndicatorLine(IND_HOVER_TOK, m_hoverTokLine);
|
if (m_editState.active)
|
||||||
m_hoverTokLine = -1;
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
if (m_editState.active || !m_hoverInside
|
// Mouse left viewport - set Arrow
|
||||||
|| !m_sci->viewport()->underMouse()) {
|
if (!m_hoverInside || !m_sci->viewport()->underMouse()) {
|
||||||
clearHoverTok();
|
if (!m_cursorOverridden) {
|
||||||
if (m_cursorOverridden) {
|
QApplication::setOverrideCursor(Qt::ArrowCursor);
|
||||||
QApplication::restoreOverrideCursor();
|
m_cursorOverridden = true;
|
||||||
m_cursorOverridden = false;
|
} else {
|
||||||
|
QApplication::changeOverrideCursor(Qt::ArrowCursor);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1062,44 +1092,56 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
if (h.inFoldCol) interactive = true;
|
if (h.inFoldCol) interactive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token box highlight
|
// Set cursor: pointing hand for interactive, arrow otherwise
|
||||||
if (!tokenHit) {
|
Qt::CursorShape desired = interactive ? Qt::PointingHandCursor : Qt::ArrowCursor;
|
||||||
clearHoverTok();
|
if (!m_cursorOverridden) {
|
||||||
} else if (line != m_hoverTokLine || t != m_hoverTokTarget) {
|
QApplication::setOverrideCursor(desired);
|
||||||
clearHoverTok();
|
|
||||||
m_hoverTokLine = line;
|
|
||||||
m_hoverTokTarget = t;
|
|
||||||
|
|
||||||
NormalizedSpan norm;
|
|
||||||
if (resolvedSpanFor(line, t, norm))
|
|
||||||
fillIndicatorCols(IND_HOVER_TOK, line, norm.start, norm.end);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interactive && !m_cursorOverridden) {
|
|
||||||
QApplication::setOverrideCursor(Qt::PointingHandCursor);
|
|
||||||
m_cursorOverridden = true;
|
m_cursorOverridden = true;
|
||||||
} else if (!interactive && m_cursorOverridden) {
|
} else {
|
||||||
QApplication::restoreOverrideCursor();
|
QApplication::changeOverrideCursor(desired);
|
||||||
m_cursorOverridden = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Live value validation ──
|
// ── Live value validation ──
|
||||||
|
|
||||||
|
void RcxEditor::setEditComment(const QString& comment) {
|
||||||
|
const LineMeta* lm = metaForLine(m_editState.line);
|
||||||
|
if (!lm) return;
|
||||||
|
|
||||||
|
QString lineText = getLineText(m_sci, m_editState.line);
|
||||||
|
ColumnSpan cs = commentSpanFor(*lm, lineText.size());
|
||||||
|
if (!cs.valid) return;
|
||||||
|
|
||||||
|
// Pad/truncate comment to fixed width
|
||||||
|
QString padded = comment.leftJustified(kColComment, ' ').left(kColComment);
|
||||||
|
|
||||||
|
long posA = posFromCol(m_sci, m_editState.line, cs.start);
|
||||||
|
long posB = posFromCol(m_sci, m_editState.line, cs.start + kColComment);
|
||||||
|
if (posB <= posA) return;
|
||||||
|
|
||||||
|
QByteArray utf8 = padded.toUtf8();
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, posB);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET,
|
||||||
|
(uintptr_t)utf8.size(), utf8.constData());
|
||||||
|
}
|
||||||
|
|
||||||
void RcxEditor::validateEditLive() {
|
void RcxEditor::validateEditLive() {
|
||||||
QString lineText = getLineText(m_sci, m_editState.line);
|
QString lineText = getLineText(m_sci, m_editState.line);
|
||||||
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
int delta = lineText.size() - m_editState.linelenAfterReplace;
|
||||||
int editedLen = m_editState.original.size() + delta;
|
int editedLen = m_editState.original.size() + delta;
|
||||||
QString text = (editedLen > 0)
|
QString text = (editedLen > 0)
|
||||||
? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString();
|
? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString();
|
||||||
bool ok;
|
QString errorMsg = fmt::validateValue(m_editState.editKind, text);
|
||||||
fmt::parseValue(m_editState.editKind, text, &ok);
|
|
||||||
showEditValidation(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RcxEditor::showEditValidation(bool valid) {
|
// Show/hide error marker (red background) and update comment
|
||||||
QColor c = valid ? QColor("#569cd6") : QColor("#e05050");
|
if (errorMsg.isEmpty()) {
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HOVER_TOK, c);
|
m_sci->markerDelete(m_editState.line, M_ERR);
|
||||||
|
setEditComment(QStringLiteral("// Enter=Save Esc=Cancel"));
|
||||||
|
} else {
|
||||||
|
m_sci->markerAdd(m_editState.line, M_ERR);
|
||||||
|
setEditComment(QStringLiteral("// ") + errorMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::setEditorFont(const QString& fontName) {
|
void RcxEditor::setEditorFont(const QString& fontName) {
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ 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_hoverTokLine = -1;
|
|
||||||
EditTarget m_hoverTokTarget = EditTarget::Name;
|
|
||||||
// ── Drag selection ──
|
// ── Drag selection ──
|
||||||
bool m_dragging = false;
|
bool m_dragging = false;
|
||||||
|
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||||
int m_dragLastLine = -1;
|
int m_dragLastLine = -1;
|
||||||
|
QPoint m_dragStartPos; // viewport coords at press
|
||||||
Qt::KeyboardModifiers m_dragInitMods = Qt::NoModifier;
|
Qt::KeyboardModifiers m_dragInitMods = Qt::NoModifier;
|
||||||
|
|
||||||
// ── Deferred click (protects multi-select on double-click) ──
|
// ── Deferred click (protects multi-select on double-click) ──
|
||||||
@@ -112,9 +112,8 @@ private:
|
|||||||
void updateEditableIndicators(int line);
|
void updateEditableIndicators(int line);
|
||||||
void applyHoverCursor();
|
void applyHoverCursor();
|
||||||
void applyHoverHighlight();
|
void applyHoverHighlight();
|
||||||
void updateEditTokenBox();
|
|
||||||
void validateEditLive();
|
void validateEditLive();
|
||||||
void showEditValidation(bool valid);
|
void setEditComment(const QString& comment);
|
||||||
|
|
||||||
// ── Refactored helpers ──
|
// ── Refactored helpers ──
|
||||||
struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; };
|
struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; };
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ namespace rcx::fmt {
|
|||||||
|
|
||||||
// ── Column layout ──
|
// ── Column layout ──
|
||||||
// COL_TYPE and COL_NAME use shared constants from core.h (kColType, kColName)
|
// COL_TYPE and COL_NAME use shared constants from core.h (kColType, kColName)
|
||||||
static constexpr int COL_TYPE = kColType;
|
static constexpr int COL_TYPE = kColType;
|
||||||
static constexpr int COL_NAME = kColName;
|
static constexpr int COL_NAME = kColName;
|
||||||
static constexpr int COL_VALUE = 22;
|
static constexpr int COL_VALUE = 22;
|
||||||
|
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) {
|
static QString fit(QString s, int w) {
|
||||||
@@ -203,26 +204,31 @@ QString readValue(const Node& node, const Provider& prov,
|
|||||||
// ── Full node line ──
|
// ── Full node line ──
|
||||||
|
|
||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine) {
|
uint64_t addr, int depth, int subLine,
|
||||||
|
const QString& comment) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = typeName(node.kind);
|
QString type = typeName(node.kind);
|
||||||
QString name = fit(node.name, COL_NAME);
|
QString name = fit(node.name, COL_NAME);
|
||||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||||
const int prefixW = COL_TYPE + COL_NAME + 4; // 2 seps × 2 chars
|
const int prefixW = COL_TYPE + COL_NAME + 4; // 2 seps × 2 chars
|
||||||
|
|
||||||
|
// Comment suffix (padded or empty)
|
||||||
|
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
|
||||||
|
: fit(comment, COL_COMMENT);
|
||||||
|
|
||||||
// Mat4x4: subLine 0..3 = rows
|
// Mat4x4: subLine 0..3 = rows
|
||||||
if (node.kind == NodeKind::Mat4x4) {
|
if (node.kind == NodeKind::Mat4x4) {
|
||||||
QString val = readValue(node, prov, addr, subLine);
|
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||||
if (subLine == 0) return ind + type + SEP + name + SEP + val;
|
if (subLine == 0) return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||||
return ind + QString(prefixW, ' ') + val;
|
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For vector types, subLine selects component
|
// For vector types, subLine selects component
|
||||||
if (subLine > 0 && (node.kind == NodeKind::Vec2 ||
|
if (subLine > 0 && (node.kind == NodeKind::Vec2 ||
|
||||||
node.kind == NodeKind::Vec3 ||
|
node.kind == NodeKind::Vec3 ||
|
||||||
node.kind == NodeKind::Vec4)) {
|
node.kind == NodeKind::Vec4)) {
|
||||||
QString val = readValue(node, prov, addr, subLine);
|
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||||
return ind + QString(prefixW, ' ') + val;
|
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hex nodes and Padding: ASCII preview + hex bytes (compact)
|
// Hex nodes and Padding: ASCII preview + hex bytes (compact)
|
||||||
@@ -234,22 +240,22 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||||
QString ascii = bytesToAscii(b, lineBytes);
|
QString ascii = bytesToAscii(b, lineBytes);
|
||||||
QString hex = bytesToHex(b, lineBytes);
|
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||||
if (subLine == 0)
|
if (subLine == 0)
|
||||||
return ind + type + SEP + ascii + SEP + hex;
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex;
|
return ind + QString(COL_TYPE + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||||
}
|
}
|
||||||
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
||||||
const int sz = sizeForKind(node.kind);
|
const int sz = sizeForKind(node.kind);
|
||||||
QByteArray b = prov.isReadable(addr, sz)
|
QByteArray b = prov.isReadable(addr, sz)
|
||||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||||
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
||||||
QString hex = bytesToHex(b, sz);
|
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
||||||
return ind + type + SEP + ascii + SEP + hex;
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString val = readValue(node, prov, addr, subLine);
|
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||||
return ind + type + SEP + name + SEP + val;
|
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Editable value (parse-friendly form for edit dialog) ──
|
// ── Editable value (parse-friendly form for edit dialog) ──
|
||||||
@@ -389,4 +395,23 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Value validation (returns error message or empty string if valid) ──
|
||||||
|
|
||||||
|
QString validateValue(NodeKind kind, const QString& text) {
|
||||||
|
QString s = text.trimmed();
|
||||||
|
if (s.isEmpty()) return {};
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
parseValue(kind, text, &ok);
|
||||||
|
if (ok) return {};
|
||||||
|
|
||||||
|
// Return byte-capacity max based on type size
|
||||||
|
const auto* m = kindMeta(kind);
|
||||||
|
if (m && m->size > 0 && m->size <= 8) {
|
||||||
|
uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1);
|
||||||
|
return QStringLiteral("0x%1 max").arg(maxVal, m->size * 2, 16, QChar('0'));
|
||||||
|
}
|
||||||
|
return QStringLiteral("invalid");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace rcx::fmt
|
} // namespace rcx::fmt
|
||||||
|
|||||||
Reference in New Issue
Block a user