mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
244420
src/examples/WinSDK.rcx
244420
src/examples/WinSDK.rcx
File diff suppressed because one or more lines are too long
@@ -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 ──
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
282
src/main.cpp
282
src/main.cpp
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ──
|
||||||
|
|||||||
Reference in New Issue
Block a user