Mat4x4 row labels, no scientific notation, per-component inline editing

- Add row0..row3 labels to Mat4x4 grid display with aligned columns
- Rewrite fmtFloat() to never use scientific notation (plain decimal + trim)
- Enable per-component inline editing for all 16 Mat4x4 floats
- Fix click-to-edit always selecting first component (thread column from hitTestTarget)
- Add isMatrixKind() helper, remove Mat4x4 from context menu edit exclusion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-02-11 10:36:15 -07:00
parent df07b61144
commit 968476b65a
5 changed files with 84 additions and 42 deletions

View File

@@ -919,6 +919,11 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
addr += subLine * 4;
editKind = NodeKind::Float;
}
// For Mat4x4 components: subLine encodes flat index (row*4 + col), 0-15
if (node.kind == NodeKind::Mat4x4 && subLine >= 0 && subLine < 16) {
addr += subLine * 4;
editKind = NodeKind::Float;
}
bool ok;
QByteArray newBytes;
@@ -1068,7 +1073,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addSeparator();
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& node.kind != NodeKind::Padding && node.kind != NodeKind::Mat4x4
&& node.kind != NodeKind::Padding
&& m_doc->provider->isWritable();
if (isEditable) {
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {

View File

@@ -134,6 +134,9 @@ inline constexpr bool isHexNode(NodeKind k) {
inline constexpr bool isVectorKind(NodeKind k) {
return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4;
}
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out;

View File

@@ -993,7 +993,7 @@ RcxEditor::HitInfo RcxEditor::hitTest(const QPoint& vp) const {
static bool hitTestTarget(QsciScintilla* sci,
const QVector<LineMeta>& meta,
const QPoint& viewportPos,
int& outLine, EditTarget& outTarget)
int& outLine, int& outCol, EditTarget& outTarget)
{
long pos = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMPOINTCLOSE,
(unsigned long)viewportPos.x(), (long)viewportPos.y());
@@ -1002,6 +1002,7 @@ static bool hitTestTarget(QsciScintilla* sci,
(unsigned long)pos);
int col = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETCOLUMN,
(unsigned long)pos);
outCol = col;
if (line < 0 || line >= meta.size()) return false;
QString lineText = getLineText(sci, line);
@@ -1183,12 +1184,12 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
}
// CommandRow: try chevron/ADDR edit or consume
if (h.nodeId == kCommandRowId) {
int tLine; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
if (t == EditTarget::TypeSelector)
emit typeSelectorRequested();
else
beginInlineEdit(t, tLine);
beginInlineEdit(t, tLine, tCol);
}
return true; // consume all CommandRow clicks
}
@@ -1197,11 +1198,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier));
// Single-click on editable token of already-selected node → edit
int tLine; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) {
int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
if (alreadySelected && plain) {
m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine);
return beginInlineEdit(t, tLine, tCol);
}
}
@@ -1276,14 +1277,14 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_sci->viewport() && !m_editState.active
&& event->type() == QEvent::MouseButtonDblClick) {
auto* me = static_cast<QMouseEvent*>(event);
int line; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), line, t)) {
int line, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), line, tCol, t)) {
m_pendingClickNodeId = 0; // cancel deferred selection change
// Narrow selection to this node before editing
auto h = hitTest(me->pos());
if (h.nodeId != 0 && h.nodeId != kCommandRowId)
emit nodeClicked(h.line, h.nodeId, Qt::NoModifier);
return beginInlineEdit(t, line);
return beginInlineEdit(t, line, tCol);
}
return true; // consume even on miss (prevent QScintilla word-select)
}
@@ -1469,14 +1470,14 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
// ── Begin inline edit ──
bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
if (target == EditTarget::TypeSelector) return false; // handled by popup, not inline edit
// Array element type and pointer target: handled by TypeSelectorPopup, not inline edit
if (target == EditTarget::ArrayElementType || target == EditTarget::PointerTarget) {
if (line < 0) {
int col;
m_sci->getCursorPosition(&line, &col);
int c;
m_sci->getCursorPosition(&line, &c);
}
auto* lm = metaForLine(line);
if (!lm) return false;
@@ -1498,10 +1499,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
m_hintLine = -1;
if (line >= 0) {
m_sci->setCursorPosition(line, 0);
m_sci->setCursorPosition(line, col >= 0 ? col : 0);
}
if (col < 0) {
m_sci->getCursorPosition(&line, &col);
}
int col;
m_sci->getCursorPosition(&line, &col);
auto* lm = metaForLine(line);
if (!lm) return false;
// Allow nodeIdx=-1 only for CommandRow editing (command bar)
@@ -1522,28 +1524,23 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
int vecComponent = 0; // which vector component (0-3)
int vecComponent = 0; // which vector/matrix component
// For vector value editing: narrow span to the clicked component
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) {
int cursorCol = col; // col from getCursorPosition above
// Find comma positions within the value span to identify components
// Helper: parse comma-separated components, narrow span to clicked one
auto narrowToComponent = [&](const QString& inner, int innerAbsStart) {
QVector<int> compStarts, compEnds;
int pos = 0;
for (int i = 0; i < trimmed.size(); i++) {
if (trimmed[i] == ',') {
for (int i = 0; i < inner.size(); i++) {
if (inner[i] == ',') {
compEnds.append(i);
// skip ", " separator
int next = i + 1;
while (next < trimmed.size() && trimmed[next] == ' ') next++;
while (next < inner.size() && inner[next] == ' ') next++;
compStarts.append(next);
}
}
compStarts.prepend(0);
compEnds.append(trimmed.size());
compEnds.append(inner.size());
// Find which component the cursor is in
int relCol = cursorCol - norm.start;
int relCol = col - innerAbsStart;
vecComponent = 0;
for (int i = 0; i < compStarts.size(); i++) {
if (relCol >= compStarts[i] && (i == compStarts.size() - 1 || relCol < compStarts[i + 1]))
@@ -1551,27 +1548,47 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
}
if (vecComponent >= compStarts.size()) vecComponent = compStarts.size() - 1;
// Narrow span to just this component
int cStart = norm.start + compStarts[vecComponent];
int cEnd = norm.start + compEnds[vecComponent];
// Trim trailing spaces from component
int cStart = innerAbsStart + compStarts[vecComponent];
int cEnd = innerAbsStart + compEnds[vecComponent];
while (cEnd > cStart && lineText[cEnd - 1] == ' ') cEnd--;
norm.start = cStart;
norm.end = cEnd;
trimmed = lineText.mid(norm.start, norm.end - norm.start);
};
// For vector value editing: narrow span to the clicked component
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) {
narrowToComponent(trimmed, norm.start);
}
// For Mat4x4 value editing: skip "rowN [...]" and narrow to clicked component
if (target == EditTarget::Value && isMatrixKind(lm->nodeKind)) {
int bracketOpen = trimmed.indexOf('[');
int bracketClose = trimmed.lastIndexOf(']');
if (bracketOpen < 0 || bracketClose <= bracketOpen)
return false;
QString inner = trimmed.mid(bracketOpen + 1, bracketClose - bracketOpen - 1);
int innerAbsStart = norm.start + bracketOpen + 1;
narrowToComponent(inner, innerAbsStart);
}
m_editState.active = true;
m_editState.line = line;
m_editState.nodeIdx = lm->nodeIdx;
m_editState.subLine = isVectorKind(lm->nodeKind) ? vecComponent : lm->subLine;
m_editState.subLine = lm->subLine;
m_editState.target = target;
m_editState.spanStart = norm.start;
m_editState.original = trimmed;
m_editState.linelenAfterReplace = lineText.size();
m_editState.editKind = lm->nodeKind;
if (isVectorKind(lm->nodeKind))
if (isVectorKind(lm->nodeKind)) {
m_editState.subLine = vecComponent;
m_editState.editKind = NodeKind::Float;
}
if (isMatrixKind(lm->nodeKind)) {
m_editState.subLine = lm->subLine * 4 + vecComponent; // flat index 0-15
m_editState.editKind = NodeKind::Float;
}
// Store fixed comment column position for value editing
// Use large lineLength so commentCol is always computed (padding added dynamically)
@@ -2040,8 +2057,8 @@ void RcxEditor::applyHoverCursor() {
}
auto h = hitTest(m_lastHoverPos);
int line; EditTarget t;
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t);
int line, hCol; EditTarget t;
bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, hCol, t);
// Skip hover span on footer lines (nothing editable)
int hoverLine = h.line;

View File

@@ -41,7 +41,7 @@ public:
// ── Inline editing ──
bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1);
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit();
void applySelectionOverlay(const QSet<uint64_t>& selIds);

View File

@@ -1,4 +1,5 @@
#include "core.h"
#include <cmath>
#include <cstring>
#include <limits>
@@ -80,8 +81,24 @@ QString fmtUInt32(uint32_t v) { return hexVal(v); }
QString fmtUInt64(uint64_t v) { return hexVal(v); }
QString fmtFloat(float v) {
QString s = QString::number(v, 'g', 4);
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
if (std::isnan(v)) return QStringLiteral("NaN");
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
// 6 significant digits — covers full single-precision range
QString s = QString::number(v, 'g', 6);
// If 'g' chose scientific notation, reformat as plain decimal
if (s.contains('e') || s.contains('E')) {
s = QString::number(v, 'f', 8);
if (s.contains('.')) {
int i = s.size() - 1;
while (i > 0 && s[i] == '0') i--;
if (s[i] == '.') i++; // keep at least one decimal digit
s.truncate(i + 1);
}
}
if (!s.contains('.'))
s += QStringLiteral(".f");
else
s += QLatin1Char('f');
@@ -268,7 +285,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
case NodeKind::Mat4x4: {
if (!display) return {}; // not editable as single value
if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
QString line = QStringLiteral("[");
QString line = QStringLiteral("row%1 [").arg(subLine);
for (int c = 0; c < 4; c++) {
if (c > 0) line += QStringLiteral(", ");
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();