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 {
// ── 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)
constexpr int SC_FOLDLEVELBASE = 0x400;
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
@@ -218,9 +261,14 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
auto suggestions = inferTypes(
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.typeHintKinds = suggestions[0].kinds;
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;
}
}

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
connect(editor, &RcxEditor::inlineEditCommitted,
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
} 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) ──
bool addedQuickConvert = false;
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
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
attachViaPlugin(entry.kind, entry.providerTarget);
// Restore saved base address (user may have navigated away from provider default)
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty())
// Restore saved base address — always override with saved value on source switch
if (entry.baseAddressFormula.isEmpty())
m_doc->tree.baseAddress = entry.baseAddress;
}
}

View File

@@ -627,6 +627,7 @@ struct LineMeta {
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
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) {

View File

@@ -912,6 +912,21 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
applySymbolColoring(result.meta, lineTexts);
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
m_hintLine = -1;
@@ -1179,6 +1194,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
}
}
}
}
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());
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
if (h.nodeId == kCommandRowId) {
int tLine, tCol; EditTarget t;
@@ -3117,6 +3153,27 @@ void RcxEditor::applyHoverCursor() {
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)
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
{
@@ -3381,6 +3438,18 @@ void RcxEditor::applyHoverCursor() {
if (h.inFoldCol) {
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) {
// Check if mouse is actually over trimmed text content (not column padding)
NormalizedSpan trimmed;

View File

@@ -86,6 +86,9 @@ signals:
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative);
void appendBytesRequested(uint64_t structId, int byteCount);
void trimHexRequested(uint64_t structId);
void appendEnumMembersRequested(uint64_t enumId, int count);
protected:
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;
}
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
return indent(depth) + QStringLiteral("};");
QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
QString footer = indent(depth) + QStringLiteral("};");
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
footer += QStringLiteral(" +10");
else
footer += QStringLiteral(" +1024 Trim");
return footer;
}
// ── Array header ──

View File

@@ -72,6 +72,7 @@ static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2};
t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2};
t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("TCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4};
t[QStringLiteral("ULONG")] = {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 (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
field.typeName == QStringLiteral("char") && firstDim <= 128) {
(field.typeName == QStringLiteral("char") ||
field.typeName == QStringLiteral("CHAR"))) {
Node n;
n.kind = NodeKind::UTF8;
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 &&
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR")) &&
firstDim <= 128) {
(field.typeName == QStringLiteral("wchar_t") ||
field.typeName == QStringLiteral("WCHAR") ||
field.typeName == QStringLiteral("TCHAR"))) {
Node n;
n.kind = NodeKind::UTF16;
n.name = field.name;
@@ -1575,6 +1578,13 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
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
auto sizeIt = parser.sizeAsserts.find(ps.name);
if (sizeIt != parser.sizeAsserts.end()) {

View File

@@ -261,7 +261,7 @@ public:
if (type == CT_TabBarTab) {
if (auto* tabBar = qobject_cast<const QTabBar*>(w)) {
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
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:hover { background: %2; }"
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"
"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:hover { background: %2; }"
"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; }
void setShimmerActive(bool on) {
@@ -1042,7 +1043,8 @@ public:
void setAlignment(Qt::Alignment a) { m_align = a; update(); }
// Colours configurable from theme
QColor colBase; // dim text (normal)
QColor colBase; // normal text
QColor colDim; // dimmed suffix text
QColor colBright; // highlight sweep
protected:
@@ -1058,7 +1060,18 @@ protected:
QColor c = colBase.isValid() ? colBase
: palette().color(QPalette::WindowText);
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;
}
@@ -1083,6 +1096,7 @@ protected:
private:
QString m_text;
QString m_dimSuffix;
bool m_shimmer = false;
float m_phase = 0.0f;
Qt::Alignment m_align = Qt::AlignLeft | Qt::AlignVCenter;
@@ -1174,8 +1188,6 @@ void MainWindow::createStatusBar() {
sb->tabRow = nullptr;
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.
// Positioned via reposition() in resizeEvent — immune to font/margin changes.
auto* grip = new ResizeGrip(this);
@@ -1194,19 +1206,38 @@ void MainWindow::createStatusBar() {
sb->setDividerColor(t.border);
m_statusLabel->colBase = t.textDim;
m_statusLabel->colDim = t.textMuted;
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) {
m_appStatus = text;
m_appStatusDim.clear();
if (!m_mcpBusy) {
m_statusLabel->setText(text);
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) {
// Cancel any pending clear — new activity extends the shimmer
if (m_mcpClearTimer) m_mcpClearTimer->stop();
@@ -1222,7 +1253,7 @@ void MainWindow::clearMcpStatus() {
m_mcpClearTimer->setSingleShot(true);
connect(m_mcpClearTimer, &QTimer::timeout, this, [this]() {
m_mcpBusy = false;
m_statusLabel->setText(m_appStatus);
m_statusLabel->setText(m_appStatus, m_appStatusDim);
m_statusLabel->setShimmerActive(false);
});
}
@@ -1248,7 +1279,7 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
"QTabWidget::pane { border: none; }"
"QTabBar { border: none; }"
"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;"
"}"
"QTabBar::tab:selected { color: %3; background: %4;"
@@ -1476,6 +1507,18 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
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) {
auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1);
@@ -1662,20 +1705,40 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
connect(ctrl, &RcxController::nodeSelected,
this, [this, ctrl, dock](int nodeIdx) {
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();
if (ap && ap->viewMode == VM_Rendered)
setAppStatus(
QString("Rendered: %1 %2")
.arg(kindToString(node.kind))
.arg(node.name));
setAppStatus(QString("Rendered: %1").arg(main));
else
setAppStatus(
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()));
setAppStatus(main, dimPart);
}
// Update all rendered panes on selection change
auto it = m_tabs.find(dock);
@@ -1711,7 +1774,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dockGuard->setWindowTitle(tabTitle(*it2));
}
rebuildWorkspaceModel();
updateWindowTitle();
@@ -1727,7 +1790,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
auto it2 = m_tabs.find(dockGuard);
if (it2 != m_tabs.end()) {
updateAllRenderedPanes(*it2);
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
dockGuard->setWindowTitle(tabTitle(*it2));
}
updateWindowTitle();
rebuildWorkspaceModel();
@@ -1795,6 +1858,23 @@ void MainWindow::setupDockTabBars() {
tp.setColor(QPalette::Link, theme.indHoverSpan);
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
for (int i = 0; i < tabBar->count(); ++i) {
auto* existing = qobject_cast<DockTabButtons*>(
@@ -1949,13 +2029,16 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
}
// Build a minimal empty struct for new documents
static int s_classCounter = 0;
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
// ── Enum: bare node with empty enumMembers, no hex children ──
if (classKeyword == QStringLiteral("enum")) {
int idx = s_classCounter++;
Node root;
root.kind = NodeKind::Struct;
root.name = "Unnamed";
root.structTypeName = "Unnamed";
root.name = QStringLiteral("UnnamedEnum%1").arg(idx);
root.structTypeName = root.name;
root.classKeyword = classKeyword;
root.parentId = 0;
root.offset = 0;
@@ -1970,10 +2053,11 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
return;
}
int idx = s_classCounter++;
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
root.structTypeName = "Unnamed";
root.name = QStringLiteral("instance%1").arg(idx);
root.structTypeName = QStringLiteral("UnnamedClass%1").arg(idx);
root.classKeyword = classKeyword;
root.parentId = 0;
root.offset = 0;
@@ -2392,6 +2476,14 @@ void MainWindow::applyTheme(const Theme& theme) {
tabBar->tabButton(i, QTabBar::RightSide));
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; }"
"QTabBar { border: none; }"
"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;"
"}"
"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::WindowText, theme.textDim);
statusBar()->setPalette(sbPal);
m_statusLabel->colBase = theme.textDim;
m_statusLabel->colDim = theme.textMuted;
m_statusLabel->colBright = theme.indHoverSpan;
}
// Status bar chrome
{
@@ -2447,8 +2542,9 @@ void MainWindow::applyTheme(const Theme& theme) {
m_workspaceTree->setPalette(tp);
m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }"
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }"
"QTreeView::branch:has-children:closed { image: url(:/vsicons/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/vsicons/chevron-down.svg); }"
"QTreeView::branch { width: 12px; }"
"QAbstractScrollArea::corner { background: %1; border: none; }"
"QHeaderView { background: %1; border: none; }"
"QHeaderView::section { background: %1; border: none; }")
@@ -2457,12 +2553,33 @@ void MainWindow::applyTheme(const Theme& theme) {
}
if (m_workspaceSearch) {
m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;"
" padding: 4px 8px; }"
"QLineEdit QToolButton { padding: 0px 4px; }"
"QLineEdit { background: %1; color: %2;"
" border: 1px solid %4;"
" padding: 4px 8px 4px 2px; }"
"QLineEdit:focus { border: 1px solid %5; }"
"QLineEdit QToolButton { padding: 0px 8px; }"
"QLineEdit QToolButton:hover { background: %3; }")
.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
@@ -2483,9 +2600,12 @@ void MainWindow::applyTheme(const Theme& theme) {
m_dockGrip->setGripColor(theme.textFaint);
if (m_workspaceDock)
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
if (m_scannerDock)
m_scannerDock->setStyleSheet(QStringLiteral(
"QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
if (m_scannerPanel)
m_scannerPanel->applyTheme(theme);
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);
wf.setFixedPitch(true);
@@ -2662,6 +2782,11 @@ void MainWindow::setEditorFont(const QString& fontName) {
m_dockTitleLabel->setFont(wf);
if (m_workspaceSearch)
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
if (m_scannerPanel)
@@ -3249,7 +3374,7 @@ QDockWidget* MainWindow::project_new(const QString& classKeyword) {
m_workspaceDock->show();
}
rebuildWorkspaceModel();
rebuildWorkspaceModelNow();
return dock;
}
@@ -3367,7 +3492,7 @@ void MainWindow::createWorkspaceDock() {
const auto& t = ThemeManager::instance().current();
auto* titleBar = new QWidget(m_workspaceDock);
titleBar->setFixedHeight(26);
titleBar->setFixedHeight(29);
titleBar->setAutoFillBackground(true);
{
QPalette tbPal = titleBar->palette();
@@ -3414,7 +3539,7 @@ void MainWindow::createWorkspaceDock() {
{
const auto& t = ThemeManager::instance().current();
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
@@ -3424,7 +3549,7 @@ void MainWindow::createWorkspaceDock() {
dockLayout->setSpacing(0);
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
{
QSettings s("Reclass", "Reclass");
@@ -3465,14 +3590,28 @@ void MainWindow::createWorkspaceDock() {
{
const auto& t = ThemeManager::instance().current();
m_workspaceSearch->setStyleSheet(QStringLiteral(
"QLineEdit { background: %1; color: %2; border: none;"
" padding: 4px 8px; }"
"QLineEdit QToolButton { padding: 0px 4px; }"
"QLineEdit { background: %1; color: %2;"
" border: 1px solid %4;"
" padding: 4px 8px 4px 2px; }"
"QLineEdit:focus { border: 1px solid %5; }"
"QLineEdit QToolButton { padding: 0px 8px; }"
"QLineEdit QToolButton:hover { background: %3; }")
.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);
// 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_workspaceModel = new QStandardItemModel(this);
@@ -3496,10 +3635,19 @@ void MainWindow::createWorkspaceDock() {
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);
if (!text.isEmpty())
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)
@@ -3517,14 +3665,16 @@ void MainWindow::createWorkspaceDock() {
m_workspaceTree->setStyleSheet(QStringLiteral(
"QTreeView { background: %1; border: none; }"
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }"
"QTreeView::branch:has-children:closed { image: url(:/vsicons/chevron-right.svg); }"
"QTreeView::branch:has-children:open { image: url(:/vsicons/chevron-down.svg); }"
"QTreeView::branch { width: 12px; }"
"QAbstractScrollArea::corner { background: %1; border: none; }"
"QHeaderView { background: %1; border: none; }"
"QHeaderView::section { background: %1; border: none; }")
.arg(t.background.name()));
}
m_workspaceTree->setIndentation(12);
dockLayout->addWidget(m_workspaceTree);
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -3691,7 +3841,9 @@ void MainWindow::createWorkspaceDock() {
const auto& item = items[0];
if (!m_tabs.contains(item.dock)) return;
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
QDockWidget* targetDock = item.dock;
@@ -3705,24 +3857,27 @@ void MainWindow::createWorkspaceDock() {
targetDock->raise();
targetDock->show();
m_activeDocDock = targetDock;
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty()
? doc->tree.nodes[item.nodeIdx].name
: doc->tree.nodes[item.nodeIdx].structTypeName;
QString structName = doc->tree.nodes[ni].structTypeName.isEmpty()
? doc->tree.nodes[ni].name
: doc->tree.nodes[ni].structTypeName;
if (!structName.isEmpty())
targetDock->setWindowTitle(structName);
rebuildWorkspaceModel();
} else if (chosen && chosen == actOpenNew && items.size() == 1) {
// Open in a brand new tab (sharing the same document)
const auto& item = items[0];
if (!m_tabs.contains(item.dock)) return;
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);
m_tabs[newDock].ctrl->setViewRootId(item.structId);
m_tabs[newDock].ctrl->refresh();
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty()
? doc->tree.nodes[item.nodeIdx].name
: doc->tree.nodes[item.nodeIdx].structTypeName;
QString structName = doc->tree.nodes[ni].structTypeName.isEmpty()
? doc->tree.nodes[ni].name
: doc->tree.nodes[ni].structTypeName;
if (!structName.isEmpty())
newDock->setWindowTitle(structName);
rebuildWorkspaceModel();
@@ -3748,8 +3903,10 @@ void MainWindow::createWorkspaceDock() {
tab.ctrl->setSuppressRefresh(true);
tab.doc->undoStack.beginMacro(QStringLiteral("Duplicate ") + item.typeName);
// Clone root node
rcx::Node root = tree.nodes[item.nodeIdx];
// Clone root node (re-lookup by ID since menu.exec() may have invalidated index)
int ni = tree.indexOfId(item.structId);
if (ni < 0) return;
rcx::Node root = tree.nodes[ni];
root.id = tree.reserveId();
root.structTypeName = newName;
root.name = newName;
@@ -3971,6 +4128,12 @@ void MainWindow::createScannerDock() {
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->applyTheme(ThemeManager::instance().current());
{
@@ -4089,6 +4252,7 @@ void MainWindow::rebuildWorkspaceModelNow() {
viewedIds.insert(it->ctrl->viewRootId());
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
auto* item = m_workspaceModel->item(i);
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
}
@@ -4096,7 +4260,9 @@ void MainWindow::rebuildWorkspaceModelNow() {
if (m_dockTitleLabel) {
int structs = 0, enums = 0;
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;
else
++structs;

View File

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

View File

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

View File

@@ -34,15 +34,15 @@ QVector<TypeSuggestion> inferTypes(
const InferHints& hints = {},
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) {
if (s.kinds.isEmpty()) return {};
const char* name = kindMeta(s.kinds[0])->typeName;
QString base = (s.kinds.size() == 1)
? QString::fromLatin1(name)
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
if (s.strength <= 2) base += QLatin1Char('?'); // moderate gets ?
return base;
const char* conf = s.strength >= 3 ? " strong" : " moderate";
return base + QLatin1String(conf);
}
// ── Implementation (header-only) ──
@@ -137,11 +137,15 @@ inline FeatureResult countFloatFeatures(uint32_t cur,
inline FeatureResult countIntFeatures(uint32_t val,
const uint8_t* minP, const uint8_t* maxP,
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;
int32_t sv = (int32_t)val;
// Feature 1: non-zero
passed += (val != 0) ? 1 : 0;
// Feature 1: non-zero and not sentinel (always passes after hard reject)
passed += 1;
// Feature 2: small absolute value
passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0;
// Feature 3: fits int16 range
@@ -189,19 +193,24 @@ inline FeatureResult countFlagFeatures(uint32_t val,
// ── Pointer feature checker ──
inline FeatureResult countPtrFeatures64(uint64_t val) {
int passed = 0, checked = 5;
// Feature 1: non-zero and not common sentinel values
passed += (val != 0 && val != 0xFFFFFFFFFFFFFFFFULL
&& val != 0x00000000FFFFFFFFULL) ? 1 : 0;
// Feature 2: canonical 48-bit address (sign-extended from bit 47)
// Hard reject: common sentinel values are never pointers
if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
return {0, 6};
int passed = 0, checked = 6;
// Feature 1: canonical 48-bit address (sign-extended from bit 47)
passed += (val <= 0x00007FFFFFFFFFFFULL
|| 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;
// Feature 4: above null guard pages (real addresses >= 64KB)
// Feature 3: above null guard pages (real addresses >= 64KB)
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;
// 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};
}

View File

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

View File

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

View File

@@ -138,17 +138,26 @@ private slots:
}
// ── formatHint ──
void formatHint_single() {
void formatHint_strong() {
TypeSuggestion s;
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() {
TypeSuggestion s;
s.kinds = {NodeKind::Float, NodeKind::Float};
s.strength = 3;
QString h = formatHint(s);
QVERIFY(h.contains("float"));
QVERIFY(h.contains("2"));
QVERIFY(h.endsWith("strong"));
}
// ── Denormal rejection ──