feat: status bar format, tab titles with source, taller tabs, pill hover, source switch base fix

- Status bar: show StructName.field +0xOFFSET with dimmed offset suffix
- Status bar: sync font to global editor font (JetBrains Mono 10pt)
- Dock tab title: include active source name (StructName — source.exe)
- Dock tabs +10% height (28→31), pane tabs (24→26), workspace title (26→29)
- Footer pills (+1024, Trim, +10): add visual hover highlight via IND_HOVER_SPAN
- Fix source switch keeping old base address for plugin providers
This commit is contained in:
IChooseYou
2026-03-08 16:29:12 -06:00
committed by IChooseYou
parent 6a4cb47ed4
commit 25afbe373b
17 changed files with 566 additions and 404436 deletions

View File

@@ -8,6 +8,49 @@ namespace rcx {
namespace { namespace {
// ── Value preview for type hints ──
// Formats raw bytes as the suggested type using existing fmt:: functions.
static QString formatPreview(const uint8_t* data, int len, const TypeSuggestion& s) {
using namespace detail;
if (s.kinds.isEmpty()) return {};
NodeKind k = s.kinds[0];
if (s.kinds.size() == 1) {
switch (k) {
case NodeKind::Float: return fmt::fmtFloat(loadF32(data));
case NodeKind::Double: return fmt::fmtDouble(loadF64(data));
case NodeKind::Int32: return fmt::fmtInt32((int32_t)loadU32(data));
case NodeKind::UInt32: return fmt::fmtUInt32(loadU32(data));
case NodeKind::Int16: return fmt::fmtInt16((int16_t)loadU16(data));
case NodeKind::UInt16: return fmt::fmtUInt16(loadU16(data));
case NodeKind::Int64: return fmt::fmtInt64((int64_t)loadU64(data));
case NodeKind::UInt64: return fmt::fmtUInt64(loadU64(data));
case NodeKind::Pointer64: return fmt::fmtPointer64(loadU64(data));
case NodeKind::Pointer32: return fmt::fmtPointer32(loadU32(data));
case NodeKind::Bool: return fmt::fmtBool(data[0]);
case NodeKind::UTF8: {
int n = std::min(len, 8);
QString s;
for (int i = 0; i < n && data[i] >= 0x20 && data[i] <= 0x7E; ++i)
s += QLatin1Char(data[i]);
return s.isEmpty() ? QString() : (QStringLiteral("\"") + s + QStringLiteral("\""));
}
default: return {};
}
}
// Split: show each part
int partSz = len / s.kinds.size();
QStringList parts;
for (int i = 0; i < s.kinds.size(); ++i) {
TypeSuggestion sub;
sub.kinds = {s.kinds[i]};
sub.score = s.score;
sub.strength = s.strength;
parts << formatPreview(data + i * partSz, partSz, sub);
}
return parts.join(QStringLiteral(", "));
}
// Scintilla fold constants (avoid including Scintilla headers in core) // Scintilla fold constants (avoid including Scintilla headers in core)
constexpr int SC_FOLDLEVELBASE = 0x400; constexpr int SC_FOLDLEVELBASE = 0x400;
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000; constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
@@ -218,9 +261,14 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0'); ? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
auto suggestions = inferTypes( auto suggestions = inferTypes(
reinterpret_cast<const uint8_t*>(b.constData()), sz); reinterpret_cast<const uint8_t*>(b.constData()), sz);
if (!suggestions.isEmpty()) { if (!suggestions.isEmpty() && suggestions[0].strength >= 2) {
lm.typeHintStart = lineText.size() + 2; // after " " gap lm.typeHintStart = lineText.size() + 2; // after " " gap
lm.typeHintKinds = suggestions[0].kinds;
lm.typeHint = formatHint(suggestions[0]); lm.typeHint = formatHint(suggestions[0]);
QString preview = formatPreview(
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
if (!preview.isEmpty())
lm.typeHint += QStringLiteral(" ") + preview;
lineText += QStringLiteral(" ") + lm.typeHint; lineText += QStringLiteral(" ") + lm.typeHint;
} }
} }

View File

@@ -246,6 +246,67 @@ void RcxController::connectEditor(RcxEditor* editor) {
} }
}); });
// Footer "+1024" button
connect(editor, &RcxEditor::appendBytesRequested,
this, [this](uint64_t structId, int byteCount) {
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
for (int i = 0; i < hex64Count; i++)
insertNode(structId, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i));
for (int i = 0; i < remainBytes; i++)
insertNode(structId, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(hex64Count + i));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
// Footer "Trim" button — remove trailing hex nodes from end of struct
connect(editor, &RcxEditor::trimHexRequested,
this, [this](uint64_t structId) {
QVector<int> children = m_doc->tree.childrenOf(structId);
if (children.isEmpty()) return;
// Sort by offset descending to find trailing hex nodes
std::sort(children.begin(), children.end(), [&](int a, int b) {
return m_doc->tree.nodes[a].offset > m_doc->tree.nodes[b].offset;
});
// Collect trailing hex nodes to remove
QVector<int> toRemove;
for (int ci : children) {
const Node& n = m_doc->tree.nodes[ci];
if (!isHexNode(n.kind)) break;
toRemove.append(ci);
}
if (toRemove.isEmpty()) return;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Trim %1 trailing hex nodes").arg(toRemove.size()));
for (int ni : toRemove)
removeNode(ni);
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
// Footer "+10" button — append enum members sequentially from highest value
connect(editor, &RcxEditor::appendEnumMembersRequested,
this, [this](uint64_t enumId, int count) {
int ni = m_doc->tree.indexOfId(enumId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
auto oldMembers = members;
for (int i = 0; i < count; i++)
members.append({QStringLiteral("Member%1").arg(nextVal + i), nextVal + i});
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{enumId, oldMembers, members}));
});
// Inline editing signals // Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted, connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text, this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
@@ -1850,6 +1911,40 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// Fall through to always-available actions // Fall through to always-available actions
} else { } else {
// ── Inference-based quick convert (from type hints) ──
if (isHexNode(node.kind) && line >= 0 && line < m_lastResult.meta.size()) {
const auto& lm = m_lastResult.meta[line];
if (!lm.typeHintKinds.isEmpty()) {
NodeKind suggested = lm.typeHintKinds[0];
if (lm.typeHintKinds.size() == 1) {
auto* m = kindMeta(suggested);
QString label = QStringLiteral("Convert to %1").arg(QString::fromLatin1(m->typeName));
menu.addAction(label, [this, nodeId, suggested]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, suggested);
});
} else {
auto* m = kindMeta(lm.typeHintKinds[0]);
QString label = QStringLiteral("Split into %1\u00D7%2")
.arg(QString::fromLatin1(m->typeName))
.arg(lm.typeHintKinds.size());
menu.addAction(label, [this, nodeId, kinds = lm.typeHintKinds]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
changeNodeKind(ni, kinds[0]);
for (int k = 1; k < kinds.size(); ++k) {
ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) break;
int next = ni + 1;
if (next < m_doc->tree.nodes.size() && isHexNode(m_doc->tree.nodes[next].kind))
changeNodeKind(next, kinds[k]);
}
});
}
menu.addSeparator();
}
}
// ── Quick-convert suggestions (top-level for fast access) ── // ── Quick-convert suggestions (top-level for fast access) ──
bool addedQuickConvert = false; bool addedQuickConvert = false;
if (node.kind == NodeKind::Hex64) { if (node.kind == NodeKind::Hex64) {
@@ -3130,8 +3225,8 @@ void RcxController::switchToSavedSource(int idx) {
// Restore formula before attach so it can be re-evaluated against the new provider // Restore formula before attach so it can be re-evaluated against the new provider
m_doc->tree.baseAddressFormula = entry.baseAddressFormula; m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
attachViaPlugin(entry.kind, entry.providerTarget); attachViaPlugin(entry.kind, entry.providerTarget);
// Restore saved base address (user may have navigated away from provider default) // Restore saved base address — always override with saved value on source switch
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty()) if (entry.baseAddressFormula.isEmpty())
m_doc->tree.baseAddress = entry.baseAddress; m_doc->tree.baseAddress = entry.baseAddress;
} }
} }

View File

@@ -627,6 +627,7 @@ struct LineMeta {
bool isStaticLine = false; // true for static field node lines bool isStaticLine = false; // true for static field node lines
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none) int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
QVector<NodeKind> typeHintKinds; // Suggested kinds from inference (empty = no hint)
}; };
inline bool isSyntheticLine(const LineMeta& lm) { inline bool isSyntheticLine(const LineMeta& lm) {

View File

@@ -912,6 +912,21 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
applySymbolColoring(result.meta, lineTexts); applySymbolColoring(result.meta, lineTexts);
applyCommandRowPills(); applyCommandRowPills();
// Footer buttons — pill styling
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind != LineKind::Footer) continue;
QString ft = getLineText(m_sci, i);
int addStart = ft.indexOf(QStringLiteral("+1024"));
if (addStart >= 0)
fillIndicatorCols(IND_CMD_PILL, i, addStart, addStart + 5);
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0)
fillIndicatorCols(IND_CMD_PILL, i, add10Start, add10Start + 3);
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0)
fillIndicatorCols(IND_CMD_PILL, i, trimStart, trimStart + 4);
}
// Reset hint line - applySelectionOverlay will repaint indicators // Reset hint line - applySelectionOverlay will repaint indicators
m_hintLine = -1; m_hintLine = -1;
@@ -1179,6 +1194,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
} }
} }
} }
} }
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) { void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
@@ -2026,6 +2042,26 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
emit marginClicked(0, h.line, me->modifiers()); emit marginClicked(0, h.line, me->modifiers());
return true; return true;
} }
// Footer buttons: +1024, +10, Trim
if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
int addStart = ft.indexOf(QStringLiteral("+1024"));
if (addStart >= 0 && h.col >= addStart && h.col < addStart + 5) {
emit appendBytesRequested(m_meta[h.line].nodeId, 1024);
return true;
}
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && h.col >= add10Start && h.col < add10Start + 3) {
emit appendEnumMembersRequested(m_meta[h.line].nodeId, 10);
return true;
}
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0 && h.col >= trimStart && h.col < trimStart + 4) {
emit trimHexRequested(m_meta[h.line].nodeId);
return true;
}
}
// CommandRow: try chevron/ADDR edit or consume // CommandRow: try chevron/ADDR edit or consume
if (h.nodeId == kCommandRowId) { if (h.nodeId == kCommandRowId) {
int tLine, tCol; EditTarget t; int tLine, tCol; EditTarget t;
@@ -3117,6 +3153,27 @@ void RcxEditor::applyHoverCursor() {
m_hoverSpanLines.append(h.line); m_hoverSpanLines.append(h.line);
} }
// Apply hover span on footer pills (+1024, +10, Trim)
if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
int addStart = ft.indexOf(QStringLiteral("+1024"));
if (addStart >= 0 && h.col >= addStart && h.col < addStart + 5) {
fillIndicatorCols(IND_HOVER_SPAN, h.line, addStart, addStart + 5);
m_hoverSpanLines.append(h.line);
}
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && h.col >= add10Start && h.col < add10Start + 3) {
fillIndicatorCols(IND_HOVER_SPAN, h.line, add10Start, add10Start + 3);
m_hoverSpanLines.append(h.line);
}
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0 && h.col >= trimStart && h.col < trimStart + 4) {
fillIndicatorCols(IND_HOVER_SPAN, h.line, trimStart, trimStart + 4);
m_hoverSpanLines.append(h.line);
}
}
// Value history popup on hover (read-only, no buttons) // Value history popup on hover (read-only, no buttons)
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead. // Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
{ {
@@ -3381,6 +3438,18 @@ void RcxEditor::applyHoverCursor() {
if (h.inFoldCol) { if (h.inFoldCol) {
desired = Qt::PointingHandCursor; // fold toggle = button desired = Qt::PointingHandCursor; // fold toggle = button
} else if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
int addStart = ft.indexOf(QStringLiteral("+1024"));
if (addStart >= 0 && h.col >= addStart && h.col < addStart + 5)
desired = Qt::PointingHandCursor;
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && h.col >= add10Start && h.col < add10Start + 3)
desired = Qt::PointingHandCursor;
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0 && h.col >= trimStart && h.col < trimStart + 4)
desired = Qt::PointingHandCursor;
} else if (tokenHit) { } else if (tokenHit) {
// Check if mouse is actually over trimmed text content (not column padding) // Check if mouse is actually over trimmed text content (not column padding)
NormalizedSpan trimmed; NormalizedSpan trimmed;

View File

@@ -86,6 +86,9 @@ signals:
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind); void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative); void relativeOffsetsChanged(bool relative);
void appendBytesRequested(uint64_t structId, int byteCount);
void trimHexRequested(uint64_t structId);
void appendEnumMembersRequested(uint64_t enumId, int count);
protected: protected:
bool eventFilter(QObject* obj, QEvent* event) override; bool eventFilter(QObject* obj, QEvent* event) override;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -163,8 +163,13 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
return ind + type + SEP + node.name + SEP + suffix; return ind + type + SEP + node.name + SEP + suffix;
} }
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
return indent(depth) + QStringLiteral("};"); QString footer = indent(depth) + QStringLiteral("};");
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
footer += QStringLiteral(" +10");
else
footer += QStringLiteral(" +1024 Trim");
return footer;
} }
// ── Array header ── // ── Array header ──

View File

@@ -72,6 +72,7 @@ static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2}; t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2};
t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2}; t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2};
t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2}; t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("TCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4}; t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4};
t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4}; t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4};
t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4}; t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4};
@@ -1366,7 +1367,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
if (firstDim <= 0) firstDim = 1; if (firstDim <= 0) firstDim = 1;
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 && if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
field.typeName == QStringLiteral("char") && firstDim <= 128) { (field.typeName == QStringLiteral("char") ||
field.typeName == QStringLiteral("CHAR"))) {
Node n; Node n;
n.kind = NodeKind::UTF8; n.kind = NodeKind::UTF8;
n.name = field.name; n.name = field.name;
@@ -1379,8 +1381,9 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
} }
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 && if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR")) && (field.typeName == QStringLiteral("wchar_t") ||
firstDim <= 128) { field.typeName == QStringLiteral("WCHAR") ||
field.typeName == QStringLiteral("TCHAR"))) {
Node n; Node n;
n.kind = NodeKind::UTF16; n.kind = NodeKind::UTF16;
n.name = field.name; n.name = field.name;
@@ -1575,6 +1578,13 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
buildFields(ctx, structId, 0, ps.fields); buildFields(ctx, structId, 0, ps.fields);
// Union: all direct children overlap at offset 0
if (ps.keyword == QStringLiteral("union")) {
QVector<int> children = tree.childrenOf(structId);
for (int ci : children)
tree.nodes[ci].offset = 0;
}
// Apply static_assert size: add tail padding if needed // Apply static_assert size: add tail padding if needed
auto sizeIt = parser.sizeAsserts.find(ps.name); auto sizeIt = parser.sizeAsserts.find(ps.name);
if (sizeIt != parser.sizeAsserts.end()) { if (sizeIt != parser.sizeAsserts.end()) {

View File

@@ -261,7 +261,7 @@ public:
if (type == CT_TabBarTab) { if (type == CT_TabBarTab) {
if (auto* tabBar = qobject_cast<const QTabBar*>(w)) { if (auto* tabBar = qobject_cast<const QTabBar*>(w)) {
if (tabBar->parent() && qobject_cast<const QMainWindow*>(tabBar->parent())) { if (tabBar->parent() && qobject_cast<const QMainWindow*>(tabBar->parent())) {
s.setHeight(28); s.setHeight(31);
} }
} }
} }
@@ -514,12 +514,12 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
// Global scrollbar styling — track matches control bg, handle is solid // Global scrollbar styling — track matches control bg, handle is solid
qApp->setStyleSheet(QStringLiteral( qApp->setStyleSheet(QStringLiteral(
"QScrollBar:vertical { background: palette(window); width: 12px; margin: 0; border: none; }" "QScrollBar:vertical { background: palette(window); width: 8px; margin: 0; border: none; }"
"QScrollBar::handle:vertical { background: %1; min-height: 20px; border: none; }" "QScrollBar::handle:vertical { background: %1; min-height: 20px; border: none; }"
"QScrollBar::handle:vertical:hover { background: %2; }" "QScrollBar::handle:vertical:hover { background: %2; }"
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }" "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"
"QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; }" "QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; }"
"QScrollBar:horizontal { background: palette(window); height: 12px; margin: 0; border: none; }" "QScrollBar:horizontal { background: palette(window); height: 8px; margin: 0; border: none; }"
"QScrollBar::handle:horizontal { background: %1; min-width: 20px; border: none; }" "QScrollBar::handle:horizontal { background: %1; min-width: 20px; border: none; }"
"QScrollBar::handle:horizontal:hover { background: %2; }" "QScrollBar::handle:horizontal:hover { background: %2; }"
"QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }" "QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }"
@@ -1027,7 +1027,8 @@ public:
}); });
} }
void setText(const QString& t) { m_text = t; update(); } void setText(const QString& t) { m_text = t; m_dimSuffix.clear(); update(); }
void setText(const QString& t, const QString& dimSuffix) { m_text = t; m_dimSuffix = dimSuffix; update(); }
QString text() const { return m_text; } QString text() const { return m_text; }
void setShimmerActive(bool on) { void setShimmerActive(bool on) {
@@ -1042,7 +1043,8 @@ public:
void setAlignment(Qt::Alignment a) { m_align = a; update(); } void setAlignment(Qt::Alignment a) { m_align = a; update(); }
// Colours configurable from theme // Colours configurable from theme
QColor colBase; // dim text (normal) QColor colBase; // normal text
QColor colDim; // dimmed suffix text
QColor colBright; // highlight sweep QColor colBright; // highlight sweep
protected: protected:
@@ -1058,7 +1060,18 @@ protected:
QColor c = colBase.isValid() ? colBase QColor c = colBase.isValid() ? colBase
: palette().color(QPalette::WindowText); : palette().color(QPalette::WindowText);
p.setPen(c); p.setPen(c);
p.drawText(r, m_align, m_text); if (m_dimSuffix.isEmpty()) {
p.drawText(r, m_align, m_text);
} else {
QFontMetrics fm(font());
int tw = fm.horizontalAdvance(m_text);
p.drawText(r, m_align, m_text);
QColor dc = colDim.isValid() ? colDim : c;
p.setPen(dc);
QRect sr = r;
sr.setLeft(r.left() + tw);
p.drawText(sr, m_align, m_dimSuffix);
}
return; return;
} }
@@ -1083,6 +1096,7 @@ protected:
private: private:
QString m_text; QString m_text;
QString m_dimSuffix;
bool m_shimmer = false; bool m_shimmer = false;
float m_phase = 0.0f; float m_phase = 0.0f;
Qt::Alignment m_align = Qt::AlignLeft | Qt::AlignVCenter; Qt::Alignment m_align = Qt::AlignLeft | Qt::AlignVCenter;
@@ -1174,8 +1188,6 @@ void MainWindow::createStatusBar() {
sb->tabRow = nullptr; sb->tabRow = nullptr;
sb->label = m_statusLabel; sb->label = m_statusLabel;
sb->setMinimumHeight(sb->fontMetrics().height() + 6);
// Grip is a direct child of the main window, NOT in the status bar layout. // Grip is a direct child of the main window, NOT in the status bar layout.
// Positioned via reposition() in resizeEvent — immune to font/margin changes. // Positioned via reposition() in resizeEvent — immune to font/margin changes.
auto* grip = new ResizeGrip(this); auto* grip = new ResizeGrip(this);
@@ -1194,19 +1206,38 @@ void MainWindow::createStatusBar() {
sb->setDividerColor(t.border); sb->setDividerColor(t.border);
m_statusLabel->colBase = t.textDim; m_statusLabel->colBase = t.textDim;
m_statusLabel->colDim = t.textMuted;
m_statusLabel->colBright = t.indHoverSpan; m_statusLabel->colBright = t.indHoverSpan;
} }
// Sync status bar font to global editor font (10pt monospace)
{
QSettings s("Reclass", "Reclass");
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
f.setFixedPitch(true);
m_statusLabel->setFont(f);
sb->setMinimumHeight(QFontMetrics(f).height() + 6);
}
} }
void MainWindow::setAppStatus(const QString& text) { void MainWindow::setAppStatus(const QString& text) {
m_appStatus = text; m_appStatus = text;
m_appStatusDim.clear();
if (!m_mcpBusy) { if (!m_mcpBusy) {
m_statusLabel->setText(text); m_statusLabel->setText(text);
m_statusLabel->setShimmerActive(false); m_statusLabel->setShimmerActive(false);
} }
} }
void MainWindow::setAppStatus(const QString& text, const QString& dimSuffix) {
m_appStatus = text;
m_appStatusDim = dimSuffix;
if (!m_mcpBusy) {
m_statusLabel->setText(text, dimSuffix);
m_statusLabel->setShimmerActive(false);
}
}
void MainWindow::setMcpStatus(const QString& text) { void MainWindow::setMcpStatus(const QString& text) {
// Cancel any pending clear — new activity extends the shimmer // Cancel any pending clear — new activity extends the shimmer
if (m_mcpClearTimer) m_mcpClearTimer->stop(); if (m_mcpClearTimer) m_mcpClearTimer->stop();
@@ -1222,7 +1253,7 @@ void MainWindow::clearMcpStatus() {
m_mcpClearTimer->setSingleShot(true); m_mcpClearTimer->setSingleShot(true);
connect(m_mcpClearTimer, &QTimer::timeout, this, [this]() { connect(m_mcpClearTimer, &QTimer::timeout, this, [this]() {
m_mcpBusy = false; m_mcpBusy = false;
m_statusLabel->setText(m_appStatus); m_statusLabel->setText(m_appStatus, m_appStatusDim);
m_statusLabel->setShimmerActive(false); m_statusLabel->setShimmerActive(false);
}); });
} }
@@ -1248,7 +1279,7 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
"QTabWidget::pane { border: none; }" "QTabWidget::pane { border: none; }"
"QTabBar { border: none; }" "QTabBar { border: none; }"
"QTabBar::tab {" "QTabBar::tab {"
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;" " background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 26px;"
" font-family: '%7'; font-size: 10pt;" " font-family: '%7'; font-size: 10pt;"
"}" "}"
"QTabBar::tab:selected { color: %3; background: %4;" "QTabBar::tab:selected { color: %3; background: %4;"
@@ -1476,6 +1507,18 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
return QStringLiteral("Untitled"); return QStringLiteral("Untitled");
} }
QString MainWindow::tabTitle(const TabState& tab) const {
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
int srcIdx = tab.ctrl->activeSourceIndex();
const auto& sources = tab.ctrl->savedSources();
if (srcIdx >= 0 && srcIdx < sources.size()) {
const auto& src = sources[srcIdx];
if (!src.displayName.isEmpty())
name += QStringLiteral(" \u2014 ") + src.displayName;
}
return name;
}
QDockWidget* MainWindow::createTab(RcxDocument* doc) { QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal); auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1); splitter->setHandleWidth(1);
@@ -1662,20 +1705,40 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
connect(ctrl, &RcxController::nodeSelected, connect(ctrl, &RcxController::nodeSelected,
this, [this, ctrl, dock](int nodeIdx) { this, [this, ctrl, dock](int nodeIdx) {
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) { if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
auto& node = ctrl->document()->tree.nodes[nodeIdx]; auto& tree = ctrl->document()->tree;
auto& node = tree.nodes[nodeIdx];
// Build "StructName.fieldName" — walk up to root struct
QString rootName;
if (node.parentId == 0) {
// Root node — use its own structTypeName or name
rootName = node.structTypeName.isEmpty() ? node.name : node.structTypeName;
} else {
// Walk up to root
int cur = nodeIdx;
while (cur >= 0 && tree.nodes[cur].parentId != 0)
cur = tree.indexOfId(tree.nodes[cur].parentId);
if (cur >= 0) {
auto& root = tree.nodes[cur];
rootName = root.structTypeName.isEmpty() ? root.name : root.structTypeName;
}
}
QString main;
if (node.parentId == 0)
main = rootName;
else if (!rootName.isEmpty())
main = rootName + "." + node.name;
else
main = node.name;
QString dimPart = QString(" +0x%1").arg(node.offset, 2, 16, QChar('0'));
auto* ap = findActiveSplitPane(); auto* ap = findActiveSplitPane();
if (ap && ap->viewMode == VM_Rendered) if (ap && ap->viewMode == VM_Rendered)
setAppStatus( setAppStatus(QString("Rendered: %1").arg(main));
QString("Rendered: %1 %2")
.arg(kindToString(node.kind))
.arg(node.name));
else else
setAppStatus( setAppStatus(main, dimPart);
QString("%1 %2 offset: 0x%3 size: %4 bytes")
.arg(kindToString(node.kind))
.arg(node.name)
.arg(node.offset, 4, 16, QChar('0'))
.arg(node.byteSize()));
} }
// Update all rendered panes on selection change // Update all rendered panes on selection change
auto it = m_tabs.find(dock); auto it = m_tabs.find(dock);
@@ -1711,7 +1774,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard); auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) { if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2); updateAllRenderedPanes(*it2);
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId())); dockGuard->setWindowTitle(tabTitle(*it2));
} }
rebuildWorkspaceModel(); rebuildWorkspaceModel();
updateWindowTitle(); updateWindowTitle();
@@ -1727,7 +1790,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard); auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) { if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2); updateAllRenderedPanes(*it2);
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId())); dockGuard->setWindowTitle(tabTitle(*it2));
} }
updateWindowTitle(); updateWindowTitle();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
@@ -1795,6 +1858,23 @@ void MainWindow::setupDockTabBars() {
tp.setColor(QPalette::Link, theme.indHoverSpan); tp.setColor(QPalette::Link, theme.indHoverSpan);
tabBar->setPalette(tp); tabBar->setPalette(tp);
// Style scroll arrows (appear when tabs overflow)
for (auto* btn : tabBar->findChildren<QToolButton*>()) {
if (btn->arrowType() == Qt::LeftArrow) {
btn->setArrowType(Qt::NoArrow);
btn->setIcon(QIcon(QStringLiteral(":/vsicons/chevron-left.svg")));
btn->setIconSize(QSize(14, 14));
} else if (btn->arrowType() == Qt::RightArrow) {
btn->setArrowType(Qt::NoArrow);
btn->setIcon(QIcon(QStringLiteral(":/vsicons/chevron-right.svg")));
btn->setIconSize(QSize(14, 14));
} else continue;
btn->setStyleSheet(QStringLiteral(
"QToolButton { background: %1; border: 1px solid %2; padding: 2px; }"
"QToolButton:hover { background: %3; }")
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
}
// Install tab buttons for any tab that doesn't have them yet // Install tab buttons for any tab that doesn't have them yet
for (int i = 0; i < tabBar->count(); ++i) { for (int i = 0; i < tabBar->count(); ++i) {
auto* existing = qobject_cast<DockTabButtons*>( auto* existing = qobject_cast<DockTabButtons*>(
@@ -1949,13 +2029,16 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
} }
// Build a minimal empty struct for new documents // Build a minimal empty struct for new documents
static int s_classCounter = 0;
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) { static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
// ── Enum: bare node with empty enumMembers, no hex children ── // ── Enum: bare node with empty enumMembers, no hex children ──
if (classKeyword == QStringLiteral("enum")) { if (classKeyword == QStringLiteral("enum")) {
int idx = s_classCounter++;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
root.name = "Unnamed"; root.name = QStringLiteral("UnnamedEnum%1").arg(idx);
root.structTypeName = "Unnamed"; root.structTypeName = root.name;
root.classKeyword = classKeyword; root.classKeyword = classKeyword;
root.parentId = 0; root.parentId = 0;
root.offset = 0; root.offset = 0;
@@ -1970,10 +2053,11 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
return; return;
} }
int idx = s_classCounter++;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
root.name = "instance"; root.name = QStringLiteral("instance%1").arg(idx);
root.structTypeName = "Unnamed"; root.structTypeName = QStringLiteral("UnnamedClass%1").arg(idx);
root.classKeyword = classKeyword; root.classKeyword = classKeyword;
root.parentId = 0; root.parentId = 0;
root.offset = 0; root.offset = 0;
@@ -2392,6 +2476,14 @@ void MainWindow::applyTheme(const Theme& theme) {
tabBar->tabButton(i, QTabBar::RightSide)); tabBar->tabButton(i, QTabBar::RightSide));
if (btns) btns->applyTheme(theme.hover); if (btns) btns->applyTheme(theme.hover);
} }
// Update scroll arrow styling
for (auto* btn : tabBar->findChildren<QToolButton*>(QString(), Qt::FindDirectChildrenOnly)) {
if (btn->icon().isNull()) continue; // skip non-arrow buttons
btn->setStyleSheet(QStringLiteral(
"QToolButton { background: %1; border: 1px solid %2; padding: 2px; }"
"QToolButton:hover { background: %3; }")
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
}
} }
} }
@@ -2402,7 +2494,7 @@ void MainWindow::applyTheme(const Theme& theme) {
"QTabWidget::pane { border: none; }" "QTabWidget::pane { border: none; }"
"QTabBar { border: none; }" "QTabBar { border: none; }"
"QTabBar::tab {" "QTabBar::tab {"
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;" " background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 26px;"
" font-family: '%7'; font-size: 10pt;" " font-family: '%7'; font-size: 10pt;"
"}" "}"
"QTabBar::tab:selected { color: %3; background: %4;" "QTabBar::tab:selected { color: %3; background: %4;"
@@ -2425,6 +2517,9 @@ void MainWindow::applyTheme(const Theme& theme) {
sbPal.setColor(QPalette::Window, theme.background); sbPal.setColor(QPalette::Window, theme.background);
sbPal.setColor(QPalette::WindowText, theme.textDim); sbPal.setColor(QPalette::WindowText, theme.textDim);
statusBar()->setPalette(sbPal); statusBar()->setPalette(sbPal);
m_statusLabel->colBase = theme.textDim;
m_statusLabel->colDim = theme.textMuted;
m_statusLabel->colBright = theme.indHoverSpan;
} }
// Status bar chrome // Status bar chrome
{ {
@@ -2447,8 +2542,9 @@ void MainWindow::applyTheme(const Theme& theme) {
m_workspaceTree->setPalette(tp); m_workspaceTree->setPalette(tp);
m_workspaceTree->setStyleSheet(QStringLiteral( m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }" "QTreeView { background: %1; border: none; }"
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }" "QTreeView::branch:has-children:closed { image: url(:/vsicons/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }" "QTreeView::branch:has-children:open { image: url(:/vsicons/chevron-down.svg); }"
"QTreeView::branch { width: 12px; }"
"QAbstractScrollArea::corner { background: %1; border: none; }" "QAbstractScrollArea::corner { background: %1; border: none; }"
"QHeaderView { background: %1; border: none; }" "QHeaderView { background: %1; border: none; }"
"QHeaderView::section { background: %1; border: none; }") "QHeaderView::section { background: %1; border: none; }")
@@ -2457,12 +2553,33 @@ void MainWindow::applyTheme(const Theme& theme) {
} }
if (m_workspaceSearch) { if (m_workspaceSearch) {
m_workspaceSearch->setStyleSheet(QStringLiteral( m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;" "QLineEdit { background: %1; color: %2;"
" padding: 4px 8px; }" " border: 1px solid %4;"
"QLineEdit QToolButton { padding: 0px 4px; }" " padding: 4px 8px 4px 2px; }"
"QLineEdit:focus { border: 1px solid %5; }"
"QLineEdit QToolButton { padding: 0px 8px; }"
"QLineEdit QToolButton:hover { background: %3; }") "QLineEdit QToolButton:hover { background: %3; }")
.arg(theme.background.name(), theme.textDim.name(), .arg(theme.background.name(), theme.textDim.name(),
theme.hover.name())); theme.hover.name(), theme.border.name(),
theme.borderFocused.name()));
}
// Workspace tab bar + separator theme update
if (m_workspaceDock) {
if (auto* tabBar = m_workspaceDock->findChild<QWidget*>("workspaceTabBar")) {
for (auto* btn : tabBar->findChildren<QToolButton*>()) {
btn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; border-bottom: 2px solid transparent;"
" padding: 4px 0; }"
"QToolButton:checked { color: %2; border-bottom: 2px solid %3; }")
.arg(theme.textMuted.name(), theme.text.name(), theme.borderFocused.name()));
}
}
if (auto* sep = m_workspaceDock->findChild<QFrame*>("workspaceSep")) {
sep->setStyleSheet(QStringLiteral("background: %1; border: none;").arg(theme.border.name()));
}
m_workspaceDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
} }
// Dock titlebar: restyle via stylesheet + close button // Dock titlebar: restyle via stylesheet + close button
@@ -2483,9 +2600,12 @@ void MainWindow::applyTheme(const Theme& theme) {
m_dockGrip->setGripColor(theme.textFaint); m_dockGrip->setGripColor(theme.textFaint);
if (m_workspaceDock) if (m_workspaceDock)
m_workspaceDock->setStyleSheet(QStringLiteral( m_workspaceDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; border-right: none; }").arg(theme.border.name())); "QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
// Scanner dock // Scanner dock
if (m_scannerDock)
m_scannerDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
if (m_scannerPanel) if (m_scannerPanel)
m_scannerPanel->applyTheme(theme); m_scannerPanel->applyTheme(theme);
if (m_scanDockTitle) if (m_scanDockTitle)
@@ -2652,7 +2772,7 @@ void MainWindow::setEditorFont(const QString& fontName) {
} }
} }
} }
// Sync workspace tree, title, and search font (10pt monospace) // Sync workspace tree, title, search, and status bar font (10pt monospace)
{ {
QFont wf(fontName, 10); QFont wf(fontName, 10);
wf.setFixedPitch(true); wf.setFixedPitch(true);
@@ -2662,6 +2782,11 @@ void MainWindow::setEditorFont(const QString& fontName) {
m_dockTitleLabel->setFont(wf); m_dockTitleLabel->setFont(wf);
if (m_workspaceSearch) if (m_workspaceSearch)
m_workspaceSearch->setFont(wf); m_workspaceSearch->setFont(wf);
if (m_statusLabel) {
m_statusLabel->setFont(wf);
auto* fsb = static_cast<FlatStatusBar*>(statusBar());
fsb->setMinimumHeight(QFontMetrics(wf).height() + 6);
}
} }
// Sync scanner panel font // Sync scanner panel font
if (m_scannerPanel) if (m_scannerPanel)
@@ -3249,7 +3374,7 @@ QDockWidget* MainWindow::project_new(const QString& classKeyword) {
m_workspaceDock->show(); m_workspaceDock->show();
} }
rebuildWorkspaceModel(); rebuildWorkspaceModelNow();
return dock; return dock;
} }
@@ -3367,7 +3492,7 @@ void MainWindow::createWorkspaceDock() {
const auto& t = ThemeManager::instance().current(); const auto& t = ThemeManager::instance().current();
auto* titleBar = new QWidget(m_workspaceDock); auto* titleBar = new QWidget(m_workspaceDock);
titleBar->setFixedHeight(26); titleBar->setFixedHeight(29);
titleBar->setAutoFillBackground(true); titleBar->setAutoFillBackground(true);
{ {
QPalette tbPal = titleBar->palette(); QPalette tbPal = titleBar->palette();
@@ -3414,7 +3539,7 @@ void MainWindow::createWorkspaceDock() {
{ {
const auto& t = ThemeManager::instance().current(); const auto& t = ThemeManager::instance().current();
m_workspaceDock->setStyleSheet(QStringLiteral( m_workspaceDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; border-right: none; }").arg(t.border.name())); "QDockWidget { border: 1px solid %1; }").arg(t.border.name()));
} }
// Container widget: search box + tree view // Container widget: search box + tree view
@@ -3424,7 +3549,7 @@ void MainWindow::createWorkspaceDock() {
dockLayout->setSpacing(0); dockLayout->setSpacing(0);
m_workspaceSearch = new QLineEdit(dockContainer); m_workspaceSearch = new QLineEdit(dockContainer);
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search...")); m_workspaceSearch->setPlaceholderText(QStringLiteral("Filter types..."));
// Clear button uses our close.svg icon instead of Qt's default circle-X // Clear button uses our close.svg icon instead of Qt's default circle-X
{ {
QSettings s("Reclass", "Reclass"); QSettings s("Reclass", "Reclass");
@@ -3465,14 +3590,28 @@ void MainWindow::createWorkspaceDock() {
{ {
const auto& t = ThemeManager::instance().current(); const auto& t = ThemeManager::instance().current();
m_workspaceSearch->setStyleSheet(QStringLiteral( m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;" "QLineEdit { background: %1; color: %2;"
" padding: 4px 8px; }" " border: 1px solid %4;"
"QLineEdit QToolButton { padding: 0px 4px; }" " padding: 4px 8px 4px 2px; }"
"QLineEdit:focus { border: 1px solid %5; }"
"QLineEdit QToolButton { padding: 0px 8px; }"
"QLineEdit QToolButton:hover { background: %3; }") "QLineEdit QToolButton:hover { background: %3; }")
.arg(t.background.name(), t.textDim.name(), .arg(t.background.name(), t.textDim.name(),
t.hover.name())); t.hover.name(), t.border.name(),
t.borderFocused.name()));
} }
m_workspaceSearch->setContentsMargins(6, 6, 6, 6);
dockLayout->addWidget(m_workspaceSearch); dockLayout->addWidget(m_workspaceSearch);
// Separator below search
{
const auto& t = ThemeManager::instance().current();
auto* sep = new QFrame(dockContainer);
sep->setObjectName(QStringLiteral("workspaceSep"));
sep->setFrameShape(QFrame::HLine);
sep->setFixedHeight(1);
sep->setStyleSheet(QStringLiteral("background: %1; border: none;").arg(t.border.name()));
dockLayout->addWidget(sep);
}
m_workspaceTree = new QTreeView(dockContainer); m_workspaceTree = new QTreeView(dockContainer);
m_workspaceModel = new QStandardItemModel(this); m_workspaceModel = new QStandardItemModel(this);
@@ -3496,10 +3635,19 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setFont(f); m_workspaceTree->setFont(f);
} }
connect(m_workspaceSearch, &QLineEdit::textChanged, this, [this](const QString& text) { m_workspaceSearchTimer = new QTimer(this);
m_workspaceSearchTimer->setSingleShot(true);
m_workspaceSearchTimer->setInterval(150);
connect(m_workspaceSearchTimer, &QTimer::timeout, this, [this]() {
QString text = m_workspaceSearch->text();
m_workspaceProxy->setFilterFixedString(text); m_workspaceProxy->setFilterFixedString(text);
if (!text.isEmpty()) if (!text.isEmpty())
m_workspaceTree->expandAll(); m_workspaceTree->expandAll();
else
m_workspaceTree->collapseAll();
});
connect(m_workspaceSearch, &QLineEdit::textChanged, this, [this]() {
m_workspaceSearchTimer->start();
}); });
// Custom delegate for rich text rendering (name bright, metadata dim) // Custom delegate for rich text rendering (name bright, metadata dim)
@@ -3517,14 +3665,16 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setStyleSheet(QStringLiteral( m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }" "QTreeView { background: %1; border: none; }"
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }" "QTreeView::branch:has-children:closed { image: url(:/vsicons/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }" "QTreeView::branch:has-children:open { image: url(:/vsicons/chevron-down.svg); }"
"QTreeView::branch { width: 12px; }"
"QAbstractScrollArea::corner { background: %1; border: none; }" "QAbstractScrollArea::corner { background: %1; border: none; }"
"QHeaderView { background: %1; border: none; }" "QHeaderView { background: %1; border: none; }"
"QHeaderView::section { background: %1; border: none; }") "QHeaderView::section { background: %1; border: none; }")
.arg(t.background.name())); .arg(t.background.name()));
} }
m_workspaceTree->setIndentation(12);
dockLayout->addWidget(m_workspaceTree); dockLayout->addWidget(m_workspaceTree);
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu); m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -3691,7 +3841,9 @@ void MainWindow::createWorkspaceDock() {
const auto& item = items[0]; const auto& item = items[0];
if (!m_tabs.contains(item.dock)) return; if (!m_tabs.contains(item.dock)) return;
RcxDocument* doc = m_tabs[item.dock].doc; RcxDocument* doc = m_tabs[item.dock].doc;
doc->tree.nodes[item.nodeIdx].collapsed = false; int ni = doc->tree.indexOfId(item.structId);
if (ni < 0) return;
doc->tree.nodes[ni].collapsed = false;
// Use the active tab if it shares the same document, else use owner // Use the active tab if it shares the same document, else use owner
QDockWidget* targetDock = item.dock; QDockWidget* targetDock = item.dock;
@@ -3705,24 +3857,27 @@ void MainWindow::createWorkspaceDock() {
targetDock->raise(); targetDock->raise();
targetDock->show(); targetDock->show();
m_activeDocDock = targetDock; m_activeDocDock = targetDock;
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty() QString structName = doc->tree.nodes[ni].structTypeName.isEmpty()
? doc->tree.nodes[item.nodeIdx].name ? doc->tree.nodes[ni].name
: doc->tree.nodes[item.nodeIdx].structTypeName; : doc->tree.nodes[ni].structTypeName;
if (!structName.isEmpty()) if (!structName.isEmpty())
targetDock->setWindowTitle(structName); targetDock->setWindowTitle(structName);
rebuildWorkspaceModel();
} else if (chosen && chosen == actOpenNew && items.size() == 1) { } else if (chosen && chosen == actOpenNew && items.size() == 1) {
// Open in a brand new tab (sharing the same document) // Open in a brand new tab (sharing the same document)
const auto& item = items[0]; const auto& item = items[0];
if (!m_tabs.contains(item.dock)) return; if (!m_tabs.contains(item.dock)) return;
RcxDocument* doc = m_tabs[item.dock].doc; RcxDocument* doc = m_tabs[item.dock].doc;
doc->tree.nodes[item.nodeIdx].collapsed = false; int ni = doc->tree.indexOfId(item.structId);
if (ni < 0) return;
doc->tree.nodes[ni].collapsed = false;
auto* newDock = createTab(doc); auto* newDock = createTab(doc);
m_tabs[newDock].ctrl->setViewRootId(item.structId); m_tabs[newDock].ctrl->setViewRootId(item.structId);
m_tabs[newDock].ctrl->refresh(); m_tabs[newDock].ctrl->refresh();
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty() QString structName = doc->tree.nodes[ni].structTypeName.isEmpty()
? doc->tree.nodes[item.nodeIdx].name ? doc->tree.nodes[ni].name
: doc->tree.nodes[item.nodeIdx].structTypeName; : doc->tree.nodes[ni].structTypeName;
if (!structName.isEmpty()) if (!structName.isEmpty())
newDock->setWindowTitle(structName); newDock->setWindowTitle(structName);
rebuildWorkspaceModel(); rebuildWorkspaceModel();
@@ -3748,8 +3903,10 @@ void MainWindow::createWorkspaceDock() {
tab.ctrl->setSuppressRefresh(true); tab.ctrl->setSuppressRefresh(true);
tab.doc->undoStack.beginMacro(QStringLiteral("Duplicate ") + item.typeName); tab.doc->undoStack.beginMacro(QStringLiteral("Duplicate ") + item.typeName);
// Clone root node // Clone root node (re-lookup by ID since menu.exec() may have invalidated index)
rcx::Node root = tree.nodes[item.nodeIdx]; int ni = tree.indexOfId(item.structId);
if (ni < 0) return;
rcx::Node root = tree.nodes[ni];
root.id = tree.reserveId(); root.id = tree.reserveId();
root.structTypeName = newName; root.structTypeName = newName;
root.name = newName; root.name = newName;
@@ -3971,6 +4128,12 @@ void MainWindow::createScannerDock() {
m_scannerDock->setTitleBarWidget(titleBar); m_scannerDock->setTitleBarWidget(titleBar);
} }
{
const auto& t = ThemeManager::instance().current();
m_scannerDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; }").arg(t.border.name()));
}
m_scannerPanel = new ScannerPanel(m_scannerDock); m_scannerPanel = new ScannerPanel(m_scannerDock);
m_scannerPanel->applyTheme(ThemeManager::instance().current()); m_scannerPanel->applyTheme(ThemeManager::instance().current());
{ {
@@ -4089,6 +4252,7 @@ void MainWindow::rebuildWorkspaceModelNow() {
viewedIds.insert(it->ctrl->viewRootId()); viewedIds.insert(it->ctrl->viewRootId());
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) { for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
auto* item = m_workspaceModel->item(i); auto* item = m_workspaceModel->item(i);
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong(); uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
item->setData(viewedIds.contains(id), Qt::UserRole + 3); item->setData(viewedIds.contains(id), Qt::UserRole + 3);
} }
@@ -4096,7 +4260,9 @@ void MainWindow::rebuildWorkspaceModelNow() {
if (m_dockTitleLabel) { if (m_dockTitleLabel) {
int structs = 0, enums = 0; int structs = 0, enums = 0;
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) { for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
if (m_workspaceModel->item(i)->data(Qt::UserRole + 2).toBool()) auto* item = m_workspaceModel->item(i);
if (!item) continue;
if (item->data(Qt::UserRole + 2).toBool())
++enums; ++enums;
else else
++structs; ++structs;

View File

@@ -4,6 +4,7 @@
#include "pluginmanager.h" #include "pluginmanager.h"
#include "scannerpanel.h" #include "scannerpanel.h"
#include "startpage.h" #include "startpage.h"
#include "workspace_model.h"
#include <QMainWindow> #include <QMainWindow>
#include <QLabel> #include <QLabel>
#include <QSplitter> #include <QSplitter>
@@ -68,6 +69,7 @@ private slots:
public: public:
// Status bar helpers — separate app / MCP channels // Status bar helpers — separate app / MCP channels
void setAppStatus(const QString& text); void setAppStatus(const QString& text);
void setAppStatus(const QString& text, const QString& dimSuffix);
void setMcpStatus(const QString& text); void setMcpStatus(const QString& text);
void clearMcpStatus(); void clearMcpStatus();
@@ -83,6 +85,7 @@ private:
QWidget* m_centralPlaceholder; QWidget* m_centralPlaceholder;
ShimmerLabel* m_statusLabel; ShimmerLabel* m_statusLabel;
QString m_appStatus; QString m_appStatus;
QString m_appStatusDim;
bool m_mcpBusy = false; bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr; QTimer* m_mcpClearTimer = nullptr;
TitleBarWidget* m_titleBar = nullptr; TitleBarWidget* m_titleBar = nullptr;
@@ -134,6 +137,7 @@ private:
TabState* tabByIndex(int index); TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); } int tabCount() const { return m_tabs.size(); }
QDockWidget* createTab(RcxDocument* doc); QDockWidget* createTab(RcxDocument* doc);
QString tabTitle(const TabState& tab) const;
void setupDockTabBars(); void setupDockTabBars();
void updateWindowTitle(); void updateWindowTitle();
void closeAllDocDocks(); void closeAllDocDocks();
@@ -165,6 +169,7 @@ private:
void rebuildWorkspaceModel(); // debounced — safe to call frequently void rebuildWorkspaceModel(); // debounced — safe to call frequently
void rebuildWorkspaceModelNow(); // immediate rebuild void rebuildWorkspaceModelNow(); // immediate rebuild
QTimer* m_workspaceRebuildTimer = nullptr; QTimer* m_workspaceRebuildTimer = nullptr;
QTimer* m_workspaceSearchTimer = nullptr;
void updateBorderColor(const QColor& color); void updateBorderColor(const QColor& color);
// Scanner dock // Scanner dock

View File

@@ -50,6 +50,7 @@
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file> <file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file> <file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="chevron-right.svg">vsicons/chevron-right.svg</file> <file alias="chevron-right.svg">vsicons/chevron-right.svg</file>
<file alias="chevron-left.svg">vsicons/chevron-left.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file> <file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file> <file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file> <file alias="symbol-class.svg">vsicons/symbol-class.svg</file>

View File

@@ -34,15 +34,15 @@ QVector<TypeSuggestion> inferTypes(
const InferHints& hints = {}, const InferHints& hints = {},
int maxResults = 3); int maxResults = 3);
// Format top suggestion as short display string (e.g. "Float×2", "Int32", "UTF8") // Format top suggestion as short display string (e.g. "ptr64 strong", "float×2 moderate")
inline QString formatHint(const TypeSuggestion& s) { inline QString formatHint(const TypeSuggestion& s) {
if (s.kinds.isEmpty()) return {}; if (s.kinds.isEmpty()) return {};
const char* name = kindMeta(s.kinds[0])->typeName; const char* name = kindMeta(s.kinds[0])->typeName;
QString base = (s.kinds.size() == 1) QString base = (s.kinds.size() == 1)
? QString::fromLatin1(name) ? QString::fromLatin1(name)
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size()); : QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
if (s.strength <= 2) base += QLatin1Char('?'); // moderate gets ? const char* conf = s.strength >= 3 ? " strong" : " moderate";
return base; return base + QLatin1String(conf);
} }
// ── Implementation (header-only) ── // ── Implementation (header-only) ──
@@ -137,11 +137,15 @@ inline FeatureResult countFloatFeatures(uint32_t cur,
inline FeatureResult countIntFeatures(uint32_t val, inline FeatureResult countIntFeatures(uint32_t val,
const uint8_t* minP, const uint8_t* maxP, const uint8_t* minP, const uint8_t* maxP,
const InferHints& h) { const InferHints& h) {
// Hard reject: zero and sentinel are never useful integers
if (val == 0 || val == 0xFFFFFFFF)
return {0, 3};
int passed = 0, checked = 3; int passed = 0, checked = 3;
int32_t sv = (int32_t)val; int32_t sv = (int32_t)val;
// Feature 1: non-zero // Feature 1: non-zero and not sentinel (always passes after hard reject)
passed += (val != 0) ? 1 : 0; passed += 1;
// Feature 2: small absolute value // Feature 2: small absolute value
passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0; passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0;
// Feature 3: fits int16 range // Feature 3: fits int16 range
@@ -189,19 +193,24 @@ inline FeatureResult countFlagFeatures(uint32_t val,
// ── Pointer feature checker ── // ── Pointer feature checker ──
inline FeatureResult countPtrFeatures64(uint64_t val) { inline FeatureResult countPtrFeatures64(uint64_t val) {
int passed = 0, checked = 5; // Hard reject: common sentinel values are never pointers
// Feature 1: non-zero and not common sentinel values if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
passed += (val != 0 && val != 0xFFFFFFFFFFFFFFFFULL return {0, 6};
&& val != 0x00000000FFFFFFFFULL) ? 1 : 0;
// Feature 2: canonical 48-bit address (sign-extended from bit 47) int passed = 0, checked = 6;
// Feature 1: canonical 48-bit address (sign-extended from bit 47)
passed += (val <= 0x00007FFFFFFFFFFFULL passed += (val <= 0x00007FFFFFFFFFFFULL
|| val >= 0xFFFF800000000000ULL) ? 1 : 0; || val >= 0xFFFF800000000000ULL) ? 1 : 0;
// Feature 3: aligned to 8 (heap/vtable allocations) // Feature 2: aligned to 8 (heap/vtable allocations)
passed += ((val & 7) == 0) ? 1 : 0; passed += ((val & 7) == 0) ? 1 : 0;
// Feature 4: above null guard pages (real addresses >= 64KB) // Feature 3: above null guard pages (real addresses >= 64KB)
passed += (val >= 0x10000) ? 1 : 0; passed += (val >= 0x10000) ? 1 : 0;
// Feature 5: has upper 32 bits (real 64-bit address, not a small constant) // Feature 4: has upper 32 bits (real 64-bit address, not a small constant)
passed += ((val >> 32) != 0) ? 1 : 0; passed += ((val >> 32) != 0) ? 1 : 0;
// Feature 5: above 4GB (in real 64-bit address space, not a 32-bit value)
passed += (val > 0x100000000ULL) ? 1 : 0;
// Feature 6: user-mode address range (not kernel 0xFFFF800000000000+)
passed += (val < 0xFFFF800000000000ULL) ? 1 : 0;
return {passed, checked}; return {passed, checked};
} }

View File

@@ -326,8 +326,9 @@ public:
if (row >= 0 && row < m_filtered->size()) { if (row >= 0 && row < m_filtered->size()) {
const auto& e = (*m_filtered)[row]; const auto& e = (*m_filtered)[row];
if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) { if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) {
QString tip = QStringLiteral("%1 (%2 B, %3 fields)\n") QString tip = QStringLiteral("%1 (0x%2 bytes, %3 fields)\n")
.arg(e.displayName).arg(e.sizeBytes).arg(e.fieldCount); .arg(e.displayName, QString::number(e.sizeBytes, 16).toUpper())
.arg(e.fieldCount);
tip += e.fieldSummary.join(QChar('\n')); tip += e.fieldSummary.join(QChar('\n'));
if (e.fieldCount > e.fieldSummary.size()) if (e.fieldCount > e.fieldSummary.size())
tip += QStringLiteral("\n..."); tip += QStringLiteral("\n...");
@@ -740,6 +741,7 @@ void TypeSelectorPopup::applyTheme(const Theme& theme) {
m_titleLabel->setPalette(pal); m_titleLabel->setPalette(pal);
m_filterEdit->setPalette(pal); m_filterEdit->setPalette(pal);
m_listView->setPalette(pal); m_listView->setPalette(pal);
m_listView->viewport()->setPalette(pal);
m_arrayCountEdit->setPalette(pal); m_arrayCountEdit->setPalette(pal);
// Esc button (snapped to corner) // Esc button (snapped to corner)
@@ -999,7 +1001,7 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
auto makeLabel = [](const TypeEntry& e) { auto makeLabel = [](const TypeEntry& e) {
QString label = e.displayName; QString label = e.displayName;
if (e.sizeBytes > 0) label += QStringLiteral(" - %1").arg(e.sizeBytes); if (e.sizeBytes > 0) label += QStringLiteral(" - 0x%1 bytes").arg(QString::number(e.sizeBytes, 16).toUpper());
return label; return label;
}; };

View File

@@ -5,6 +5,7 @@
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QStandardItem> #include <QStandardItem>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
#include <QSortFilterProxyModel>
#include <QPainter> #include <QPainter>
#include <QApplication> #include <QApplication>
#include <algorithm> #include <algorithm>
@@ -43,6 +44,7 @@ inline void buildStructChildren(QStandardItem* item,
}; };
for (int mi : members) { for (int mi : members) {
if (mi < 0 || mi >= tree->nodes.size()) continue;
const Node& m = tree->nodes[mi]; const Node& m = tree->nodes[mi];
if (isHexPad(m.kind)) continue; if (isHexPad(m.kind)) continue;
QString childDisplay = QStringLiteral("%1 %2") QString childDisplay = QStringLiteral("%1 %2")
@@ -75,10 +77,11 @@ inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
// Build a new item for a type entry. // Build a new item for a type entry.
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree, inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
void* subPtr) { void* subPtr) {
static const QIcon enumIcon(":/vsicons/symbol-enum.svg");
static const QIcon structIcon(":/vsicons/symbol-structure.svg");
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum"); bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
auto* item = new QStandardItem( auto* item = new QStandardItem(
QIcon(isEnum ? ":/vsicons/symbol-enum.svg" isEnum ? enumIcon : structIcon,
: ":/vsicons/symbol-structure.svg"),
typeDisplayString(node, tree)); typeDisplayString(node, tree));
item->setData(QVariant::fromValue(subPtr), Qt::UserRole); item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1); item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
@@ -145,7 +148,9 @@ inline void syncProjectExplorer(QStandardItemModel* model,
// Remove stale items (backwards) // Remove stale items (backwards)
for (int i = model->rowCount() - 1; i >= 0; --i) { for (int i = model->rowCount() - 1; i >= 0; --i) {
uint64_t id = model->item(i)->data(Qt::UserRole + 1).toULongLong(); auto* item = model->item(i);
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
if (!desiredMap.contains(id)) if (!desiredMap.contains(id))
model->removeRow(i); model->removeRow(i);
} }
@@ -201,6 +206,8 @@ public:
m_selected = t.selected; m_selected = t.selected;
m_accent = t.borderFocused; // left accent bar m_accent = t.borderFocused; // left accent bar
m_bg = t.background; m_bg = t.background;
m_badgeBg = t.backgroundAlt;
m_badgeText = t.textDim;
} }
QSize sizeHint(const QStyleOptionViewItem& option, QSize sizeHint(const QStyleOptionViewItem& option,
@@ -234,40 +241,62 @@ public:
QString fullText = index.data(Qt::DisplayRole).toString(); QString fullText = index.data(Qt::DisplayRole).toString();
QRect textRect = opt.rect.adjusted(4, 0, -4, 0); QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
// Draw icon for top-level items // Letter badge (S/E for top-level, F for children)
if (!isChild) { {
bool viewed = index.data(Qt::UserRole + 3).toBool(); QChar letter = 'F';
QVariant iconVar = index.data(Qt::DecorationRole); if (!isChild) {
if (iconVar.isValid()) { bool isEnum = index.data(Qt::UserRole + 2).toBool();
QIcon icon = iconVar.value<QIcon>(); letter = isEnum ? 'E' : 'S';
int iconSz = opt.fontMetrics.height();
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
icon.paint(painter, QRect(textRect.x(), iconY, iconSz, iconSz),
Qt::AlignCenter, viewed ? QIcon::Normal : QIcon::Disabled);
textRect.setLeft(textRect.left() + iconSz + 4);
} }
int sz = opt.fontMetrics.height();
int y = textRect.y() + (textRect.height() - sz) / 2;
QRect badge(textRect.x(), y, sz, sz);
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setRenderHint(QPainter::TextAntialiasing, true);
painter->setPen(Qt::NoPen);
painter->setBrush(m_badgeBg);
painter->drawRoundedRect(badge, 3, 3);
QColor letterCol = m_badgeText;
if (!isChild && !index.data(Qt::UserRole + 3).toBool())
letterCol.setAlpha(100);
painter->setPen(letterCol);
QFont bf = opt.font;
bf.setBold(true);
painter->setFont(bf);
painter->drawText(badge, Qt::AlignCenter, letter);
painter->setRenderHint(QPainter::Antialiasing, false);
textRect.setLeft(textRect.left() + sz + 4);
} }
painter->setFont(opt.font); painter->setFont(opt.font);
if (!isChild) { if (!isChild) {
// Top-level: "StructName — 3" // Top-level: "StructName — 3" → name left, count pill right
int dashPos = fullText.indexOf(QChar(0x2014)); int dashPos = fullText.indexOf(QChar(0x2014));
if (dashPos > 1) { QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
QString name = fullText.left(dashPos - 1); QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
QString meta = fullText.mid(dashPos - 1);
painter->setPen(m_text); if (!count.isEmpty()) {
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name); int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
int nameW = opt.fontMetrics.horizontalAdvance(name); int ch = opt.fontMetrics.height();
int cy = textRect.y() + (textRect.height() - ch) / 2;
QRect metaRect = textRect; QRect pill(textRect.right() - cw, cy, cw, ch);
metaRect.setLeft(textRect.left() + nameW); // Draw name clipped before pill
if (pill.left() > textRect.left() + 4) {
QRect nameRect = textRect;
nameRect.setRight(pill.left() - 4);
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
painter->setPen(m_text);
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
}
painter->setPen(Qt::NoPen);
painter->setBrush(m_badgeBg);
painter->drawRect(pill);
painter->setPen(m_textMuted); painter->setPen(m_textMuted);
painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta); painter->drawText(pill, Qt::AlignCenter, count);
} else { } else {
painter->setPen(m_text); painter->setPen(m_text);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
} }
} else { } else {
// Child: "TypeName fieldName" // Child: "TypeName fieldName"
@@ -296,6 +325,7 @@ public:
private: private:
QColor m_text, m_textDim, m_textMuted, m_syntaxType; QColor m_text, m_textDim, m_textMuted, m_syntaxType;
QColor m_hover, m_selected, m_accent, m_bg; QColor m_hover, m_selected, m_accent, m_bg;
QColor m_badgeBg, m_badgeText;
}; };
} // namespace rcx } // namespace rcx

View File

@@ -138,17 +138,26 @@ private slots:
} }
// ── formatHint ── // ── formatHint ──
void formatHint_single() { void formatHint_strong() {
TypeSuggestion s; TypeSuggestion s;
s.kinds = {NodeKind::Float}; s.kinds = {NodeKind::Float};
QCOMPARE(formatHint(s), QStringLiteral("float")); s.strength = 3;
QCOMPARE(formatHint(s), QStringLiteral("float strong"));
}
void formatHint_moderate() {
TypeSuggestion s;
s.kinds = {NodeKind::Float};
s.strength = 2;
QCOMPARE(formatHint(s), QStringLiteral("float moderate"));
} }
void formatHint_split() { void formatHint_split() {
TypeSuggestion s; TypeSuggestion s;
s.kinds = {NodeKind::Float, NodeKind::Float}; s.kinds = {NodeKind::Float, NodeKind::Float};
s.strength = 3;
QString h = formatHint(s); QString h = formatHint(s);
QVERIFY(h.contains("float")); QVERIFY(h.contains("float"));
QVERIFY(h.contains("2")); QVERIFY(h.contains("2"));
QVERIFY(h.endsWith("strong"));
} }
// ── Denormal rejection ── // ── Denormal rejection ──