mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
6 Commits
snapshot-2
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f751e751 | ||
|
|
0a19789a9d | ||
|
|
62a68bef80 | ||
|
|
4941f860b6 | ||
|
|
c45d51d736 | ||
|
|
5b46065403 |
35
README.md
35
README.md
@@ -5,7 +5,7 @@
|
||||
<img src="docs/RECLASS_DARKMODE.svg" alt="Reclass" height="170" />
|
||||
</picture>
|
||||
|
||||
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>A complete overhaul of the popular "reclassing" tools**
|
||||
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>Built from scratch as a modern replacement for ReClass.NET and ReClassEx**
|
||||
|
||||
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
|
||||
|
||||
@@ -16,31 +16,32 @@
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
|
||||
|
||||
Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
|
||||
|
||||
---
|
||||
Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
|
||||
|
||||
## Features
|
||||
|
||||
- **Structured binary view** — render raw bytes as typed fields (integers, floats, pointers, vectors, matrices, strings, booleans, padding)
|
||||
- **Struct & array nesting** — define nested structs and arrays with collapsible fold regions
|
||||
- **Enums & bitfields** — define enums and bitfield types with named members, inline editing, and auto-sort
|
||||
- **Inline editing** — click to edit type names, field names, values, and base addresses directly in the editor
|
||||
- **Undo/redo** — full undo history for all mutations via command stack
|
||||
- **Multi-document tabs** — open multiple projects simultaneously in MDI sub-windows
|
||||
- **Split views** — multiple synchronized editor panes over the same document
|
||||
- **Type autocomplete** — popup type picker when changing field kinds
|
||||
- **Hex + ASCII margins** — raw byte previews alongside the structured view
|
||||
- **Value history & heatmap** — track value changes over time with color-coded heat indicators
|
||||
- **Disassembly preview** — hover over code pointers to see decoded instructions
|
||||
- **C/C++ code generation** — export structs as compilable C/C++ headers
|
||||
- **Import / export** — PDB import (Windows), ReClass XML import/export, C/C++ source import
|
||||
- **Themes** — built-in theme editor with multiple presets
|
||||
- **MCP bridge** — expose all tool functionality to AI clients via Model Context Protocol
|
||||
- **Plugin system** — extend with custom data source providers via DLL plugins; following ship by default
|
||||
- **Plugin system** — extend with custom data source providers via DLL plugins; the following ship by default:
|
||||
- **Process plugin** — access memory of live processes on Windows and Linux
|
||||
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
|
||||
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Process memory section enumeration
|
||||
@@ -51,8 +52,6 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] Display RTTI information
|
||||
|
||||
---
|
||||
|
||||
## Data Sources
|
||||
|
||||
- **File** — open any binary file and inspect its contents as structured data
|
||||
@@ -60,8 +59,6 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
|
||||
- **Remote Process** — read another process's memory via shared memory
|
||||
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
@@ -70,11 +67,9 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server does not start by default and can be toggled from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server starts automatically on launch and can be toggled from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code). A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -87,13 +82,11 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Qt 6** with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
|
||||
- **Qt 6** (or Qt 5) with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
|
||||
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
|
||||
- **Ninja** — bundled with the Qt installer
|
||||
|
||||
@@ -125,15 +118,11 @@ The build script auto-detects your Qt install location.
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alternatives
|
||||
|
||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License</sub>
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,15 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
for (const auto& m : node.enumMembers)
|
||||
maxNameLen = qMax(maxNameLen, (int)m.first.size());
|
||||
|
||||
for (int mi = 0; mi < node.enumMembers.size(); mi++) {
|
||||
// Build display order sorted by value
|
||||
QVector<int> order(node.enumMembers.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
||||
return node.enumMembers[a].second < node.enumMembers[b].second;
|
||||
});
|
||||
|
||||
for (int oi = 0; oi < order.size(); oi++) {
|
||||
int mi = order[oi];
|
||||
const auto& m = node.enumMembers[mi];
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
@@ -304,6 +312,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.subLine = mi;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.isMemberLine = true;
|
||||
lm.nodeKind = NodeKind::UInt32;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.markerMask = 0;
|
||||
@@ -334,6 +343,57 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
return;
|
||||
}
|
||||
|
||||
// Bitfield with members: render name : width = value lines
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("bitfield")
|
||||
&& !node.bitfieldMembers.isEmpty()) {
|
||||
int childDepth = depth + 1;
|
||||
int maxNameLen = 4;
|
||||
for (const auto& m : node.bitfieldMembers)
|
||||
maxNameLen = qMax(maxNameLen, (int)m.name.size());
|
||||
|
||||
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
|
||||
const auto& m = node.bitfieldMembers[mi];
|
||||
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
|
||||
m.bitOffset, m.bitWidth);
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.subLine = mi;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.isMemberLine = true;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.markerMask = 0;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
|
||||
childDepth, maxNameLen), lm);
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (!isArrayChild) {
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Footer;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = isRootHeader;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
int sz = sizeForKind(node.elementKind);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const QVector<int>& children = childIndices(state, node.id);
|
||||
|
||||
int childDepth = depth + 1;
|
||||
@@ -741,7 +801,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
}
|
||||
|
||||
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {");
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {");
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = -1;
|
||||
|
||||
@@ -250,6 +250,15 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
if (text.isEmpty()) break;
|
||||
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
// Enum member name edit
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum")
|
||||
&& subLine >= 0 && subLine < node.enumMembers.size()) {
|
||||
auto members = node.enumMembers;
|
||||
members[subLine].first = text;
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{node.id, node.enumMembers, members}));
|
||||
break;
|
||||
}
|
||||
// ASCII edit on Hex nodes
|
||||
if (isHexPreview(node.kind)) {
|
||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
|
||||
@@ -321,9 +330,27 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditTarget::Value:
|
||||
case EditTarget::Value: {
|
||||
// Enum member value edit
|
||||
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum")
|
||||
&& subLine >= 0 && subLine < node.enumMembers.size()) {
|
||||
bool ok;
|
||||
int64_t val = text.toLongLong(&ok);
|
||||
if (!ok) val = text.toLongLong(&ok, 16);
|
||||
if (ok) {
|
||||
auto members = node.enumMembers;
|
||||
members[subLine].second = val;
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{node.id, node.enumMembers, members}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
|
||||
break;
|
||||
}
|
||||
case EditTarget::BaseAddress: {
|
||||
QString s = text.trimmed();
|
||||
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
||||
@@ -569,9 +596,10 @@ void RcxController::refresh() {
|
||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||
QSet<uint64_t> valid;
|
||||
for (uint64_t id : m_selIds) {
|
||||
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask);
|
||||
if (m_doc->tree.indexOfId(nodeId) >= 0)
|
||||
valid.insert(id); // Keep original ID (with footer/array bits if present)
|
||||
valid.insert(id); // Keep original ID (with footer/array/member bits if present)
|
||||
}
|
||||
m_selIds = valid;
|
||||
|
||||
@@ -1145,6 +1173,10 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
m_valueHistory.remove(c.nodeId);
|
||||
for (int ci : tree.subtreeIndices(c.nodeId))
|
||||
m_valueHistory.remove(tree.nodes[ci].id);
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeEnumMembers>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0)
|
||||
tree.nodes[idx].enumMembers = isUndo ? c.oldMembers : c.newMembers;
|
||||
}
|
||||
}, command);
|
||||
|
||||
@@ -1379,6 +1411,86 @@ void RcxController::splitHexNode(uint64_t nodeId) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::toggleBitfieldBit(uint64_t nodeId, int memberIdx) {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
const Node& node = m_doc->tree.nodes[ni];
|
||||
if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return;
|
||||
if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return;
|
||||
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
||||
|
||||
const auto& bm = node.bitfieldMembers[memberIdx];
|
||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||
int containerSize = sizeForKind(node.elementKind);
|
||||
if (containerSize <= 0) containerSize = 4;
|
||||
|
||||
QByteArray oldBytes(containerSize, 0);
|
||||
m_doc->provider->read(addr, oldBytes.data(), containerSize);
|
||||
|
||||
QByteArray newBytes = oldBytes;
|
||||
// Toggle the bit
|
||||
int byteIdx = bm.bitOffset / 8;
|
||||
int bitInByte = bm.bitOffset % 8;
|
||||
if (byteIdx < containerSize)
|
||||
newBytes[byteIdx] = newBytes[byteIdx] ^ (1 << bitInByte);
|
||||
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::editBitfieldValue(uint64_t nodeId, int memberIdx) {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
const Node& node = m_doc->tree.nodes[ni];
|
||||
if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return;
|
||||
if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return;
|
||||
if (!m_doc->provider || !m_doc->provider->isWritable()) return;
|
||||
|
||||
const auto& bm = node.bitfieldMembers[memberIdx];
|
||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||
int containerSize = sizeForKind(node.elementKind);
|
||||
if (containerSize <= 0) containerSize = 4;
|
||||
|
||||
// Read current value
|
||||
uint64_t curVal = fmt::extractBits(*m_doc->provider, addr, node.elementKind,
|
||||
bm.bitOffset, bm.bitWidth);
|
||||
uint64_t maxVal = (bm.bitWidth >= 64) ? UINT64_MAX : ((1ULL << bm.bitWidth) - 1);
|
||||
|
||||
bool ok = false;
|
||||
QString input = QInputDialog::getText(nullptr,
|
||||
QStringLiteral("Edit Bitfield Value"),
|
||||
QStringLiteral("%1 (%2 bits, max %3):")
|
||||
.arg(bm.name).arg(bm.bitWidth).arg(maxVal),
|
||||
QLineEdit::Normal,
|
||||
QString::number(curVal), &ok);
|
||||
if (!ok || input.isEmpty()) return;
|
||||
|
||||
// Parse value (support hex with 0x prefix)
|
||||
uint64_t newVal;
|
||||
if (input.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||
newVal = input.mid(2).toULongLong(&ok, 16);
|
||||
else
|
||||
newVal = input.toULongLong(&ok, 10);
|
||||
if (!ok) return;
|
||||
newVal &= maxVal;
|
||||
|
||||
QByteArray oldBytes(containerSize, 0);
|
||||
m_doc->provider->read(addr, oldBytes.data(), containerSize);
|
||||
|
||||
// Read-modify-write: clear target bits and set new value
|
||||
QByteArray newBytes = oldBytes;
|
||||
uint64_t container = 0;
|
||||
memcpy(&container, newBytes.constData(), qMin(containerSize, (int)sizeof(container)));
|
||||
uint64_t mask = maxVal << bm.bitOffset;
|
||||
container = (container & ~mask) | ((newVal & maxVal) << bm.bitOffset);
|
||||
memcpy(newBytes.data(), &container, qMin(containerSize, (int)sizeof(container)));
|
||||
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
int subLine, const QPoint& globalPos) {
|
||||
auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); };
|
||||
@@ -1535,6 +1647,31 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
uint64_t nodeId = node.id;
|
||||
uint64_t parentId = node.parentId;
|
||||
|
||||
// ── Member line: enum or bitfield member ──
|
||||
bool isEnumMember = node.resolvedClassKeyword() == QStringLiteral("enum")
|
||||
&& !node.enumMembers.isEmpty()
|
||||
&& subLine >= 0 && subLine < node.enumMembers.size();
|
||||
bool isBitfieldMember = node.resolvedClassKeyword() == QStringLiteral("bitfield")
|
||||
&& !node.bitfieldMembers.isEmpty()
|
||||
&& subLine >= 0 && subLine < node.bitfieldMembers.size();
|
||||
|
||||
if (isEnumMember || isBitfieldMember) {
|
||||
if (isBitfieldMember) {
|
||||
const auto& bm = node.bitfieldMembers[subLine];
|
||||
if (bm.bitWidth == 1) {
|
||||
menu.addAction("Toggle Bit", [this, nodeId, subLine]() {
|
||||
toggleBitfieldBit(nodeId, subLine);
|
||||
});
|
||||
} else {
|
||||
menu.addAction("Edit Value...", [this, nodeId, subLine]() {
|
||||
editBitfieldValue(nodeId, subLine);
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
}
|
||||
// Fall through to always-available actions
|
||||
} else {
|
||||
|
||||
// Quick-convert suggestions for Hex nodes
|
||||
bool addedQuickConvert = false;
|
||||
if (node.kind == NodeKind::Hex64) {
|
||||
@@ -1756,6 +1893,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
} // else (non-member node actions)
|
||||
}
|
||||
|
||||
// ── Always-available actions ──
|
||||
@@ -1885,6 +2023,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
return nid | kFooterIdBit;
|
||||
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
||||
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
||||
if (lm.isMemberLine && lm.subLine >= 0)
|
||||
return makeMemberSelId(nid, lm.subLine);
|
||||
return nid;
|
||||
};
|
||||
|
||||
@@ -1933,8 +2073,9 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
|
||||
if (m_selIds.size() == 1) {
|
||||
uint64_t sid = *m_selIds.begin();
|
||||
// Strip footer/array bits for node lookup
|
||||
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
|
||||
// Strip footer/array/member bits for node lookup
|
||||
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask));
|
||||
if (idx >= 0) emit nodeSelected(idx);
|
||||
}
|
||||
}
|
||||
@@ -1970,7 +2111,7 @@ void RcxController::updateCommandRow() {
|
||||
addr = QStringLiteral("0x") +
|
||||
QString::number(m_doc->tree.baseAddress, 16).toUpper();
|
||||
|
||||
QString row = QStringLiteral("%1 \u00B7 %2")
|
||||
QString row = QStringLiteral("%1 %2")
|
||||
.arg(elide(src, 40), elide(addr, 24));
|
||||
|
||||
// Build row 2: root class type + name (uses current view root)
|
||||
@@ -2001,7 +2142,7 @@ void RcxController::updateCommandRow() {
|
||||
if (row2.isEmpty())
|
||||
row2 = QStringLiteral("struct NoName {");
|
||||
|
||||
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2;
|
||||
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
||||
|
||||
for (auto* ed : m_editors) {
|
||||
ed->setCommandRowText(combined);
|
||||
|
||||
@@ -98,6 +98,8 @@ public:
|
||||
void duplicateNode(int nodeIdx);
|
||||
void convertToTypedPointer(uint64_t nodeId);
|
||||
void splitHexNode(uint64_t nodeId);
|
||||
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
|
||||
void editBitfieldValue(uint64_t nodeId, int memberIdx);
|
||||
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
||||
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
||||
|
||||
132
src/core.h
132
src/core.h
@@ -179,6 +179,14 @@ enum Marker : int {
|
||||
M_ACCENT = 9,
|
||||
};
|
||||
|
||||
// ── Bitfield member (name + bit position + width within a container) ──
|
||||
|
||||
struct BitfieldMember {
|
||||
QString name;
|
||||
uint8_t bitOffset = 0; // position from LSB within the container
|
||||
uint8_t bitWidth = 1; // number of bits (1..64)
|
||||
};
|
||||
|
||||
// ── Node ──
|
||||
|
||||
struct Node {
|
||||
@@ -197,6 +205,7 @@ struct Node {
|
||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||
int viewIndex = 0; // Array: current view offset (transient)
|
||||
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
QVector<BitfieldMember> bitfieldMembers; // Bitfield: per-bit member definitions
|
||||
|
||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||
int byteSize() const {
|
||||
@@ -208,6 +217,12 @@ struct Node {
|
||||
if (elemSz <= 0) return 0;
|
||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||
}
|
||||
case NodeKind::Struct:
|
||||
if (classKeyword == QStringLiteral("bitfield")) {
|
||||
int sz = sizeForKind(elementKind);
|
||||
return sz > 0 ? sz : 4;
|
||||
}
|
||||
return 0;
|
||||
default: return sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
@@ -240,6 +255,17 @@ struct Node {
|
||||
}
|
||||
o["enumMembers"] = arr;
|
||||
}
|
||||
if (!bitfieldMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : bitfieldMembers) {
|
||||
QJsonObject bm;
|
||||
bm["name"] = m.name;
|
||||
bm["bitOffset"] = m.bitOffset;
|
||||
bm["bitWidth"] = m.bitWidth;
|
||||
arr.append(bm);
|
||||
}
|
||||
o["bitfieldMembers"] = arr;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static Node fromJson(const QJsonObject& o) {
|
||||
@@ -265,6 +291,17 @@ struct Node {
|
||||
em["value"].toString("0").toLongLong()});
|
||||
}
|
||||
}
|
||||
if (o.contains("bitfieldMembers")) {
|
||||
QJsonArray arr = o["bitfieldMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject bm = v.toObject();
|
||||
BitfieldMember m;
|
||||
m.name = bm["name"].toString();
|
||||
m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0);
|
||||
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
|
||||
n.bitfieldMembers.append(m);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -512,6 +549,18 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||
}
|
||||
|
||||
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
|
||||
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 48;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
|
||||
|
||||
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
|
||||
}
|
||||
inline int memberSubFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||
}
|
||||
|
||||
struct LineMeta {
|
||||
int nodeIdx = -1;
|
||||
uint64_t nodeId = 0;
|
||||
@@ -541,6 +590,7 @@ struct LineMeta {
|
||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member lines
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -585,13 +635,15 @@ namespace cmd {
|
||||
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
||||
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
||||
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
||||
struct ChangeEnumMembers { uint64_t nodeId;
|
||||
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -621,13 +673,13 @@ inline constexpr int kMaxNameW = 128; // Maximum name column width
|
||||
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
|
||||
|
||||
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
return {ind, ind + typeW, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {};
|
||||
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
@@ -642,6 +694,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
if (lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex uses nameW for ASCII column (same as regular name column)
|
||||
@@ -660,6 +713,27 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
return {start, start + valWidth, true};
|
||||
}
|
||||
|
||||
// Member line spans (enum "name = value", bitfield "name : N = value")
|
||||
inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int eq = lineText.indexOf(QLatin1String(" = "), ind);
|
||||
if (eq < 0) return {};
|
||||
int nameEnd = eq;
|
||||
while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--;
|
||||
return {ind, nameEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int eq = lineText.indexOf(QLatin1String(" = "));
|
||||
if (eq < 0) return {};
|
||||
int valStart = eq + 3;
|
||||
int valEnd = lineText.size();
|
||||
while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--;
|
||||
return {valStart, valEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
@@ -681,30 +755,14 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
// Line format: "source▾ · 0x140000000"
|
||||
|
||||
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
// Source label ends at the ▾ dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (tag < 0) return {};
|
||||
int start = tag + 3; // after " · "
|
||||
// Scan to next " · " separator (or end of line) to support formulas with spaces
|
||||
int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start);
|
||||
int end = (nextSep >= 0) ? nextSep : lineText.size();
|
||||
// Trim trailing whitespace
|
||||
while (end > start && lineText[end - 1].isSpace()) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
// ── CommandRow root-class spans ──
|
||||
@@ -723,6 +781,25 @@ inline int commandRowRootStart(const QString& lineText) {
|
||||
return best;
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
// Address starts at "0x" after the source dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = lineText.indexOf(QStringLiteral("0x"), arrow);
|
||||
if (start < 0) {
|
||||
// Formula mode: address is between arrow and root keyword
|
||||
start = arrow + 1;
|
||||
while (start < lineText.size() && lineText[start].isSpace()) start++;
|
||||
}
|
||||
// End at root keyword (struct/class/enum) or end of line
|
||||
int rootStart = commandRowRootStart(lineText);
|
||||
int end = (rootStart > start) ? rootStart : lineText.size();
|
||||
// Trim trailing whitespace
|
||||
while (end > start && lineText[end - 1].isSpace()) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
|
||||
int start = commandRowRootStart(lineText);
|
||||
if (start < 0) return {};
|
||||
@@ -893,6 +970,11 @@ namespace fmt {
|
||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||
QString validateValue(NodeKind kind, const QString& text);
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW);
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth);
|
||||
} // namespace fmt
|
||||
|
||||
// ── Compose function forward declaration ──
|
||||
|
||||
@@ -880,7 +880,7 @@ void RcxEditor::reformatMargins() {
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
auto& lm = m_meta[i];
|
||||
|
||||
if (lm.isContinuation) {
|
||||
if (lm.isContinuation || lm.isMemberLine) {
|
||||
lm.offsetText = QStringLiteral(" \u00B7 ");
|
||||
} else if (lm.offsetText.isEmpty()) {
|
||||
continue;
|
||||
@@ -1079,8 +1079,11 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
for (uint64_t selId : selIds) {
|
||||
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
||||
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
||||
bool isMemberSel = (selId & kMemberBit) != 0;
|
||||
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
||||
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||
int memberSubLine = isMemberSel ? memberSubFromSelId(selId) : -1;
|
||||
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask);
|
||||
auto it = m_nodeLineIndex.constFind(nodeId);
|
||||
if (it == m_nodeLineIndex.constEnd()) continue;
|
||||
for (int ln : *it) {
|
||||
@@ -1094,8 +1097,13 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
||||
continue;
|
||||
} else if (m_meta[ln].isArrayElement) {
|
||||
// Plain nodeId selection shouldn't highlight individual array elements
|
||||
// (the header line is enough)
|
||||
continue;
|
||||
}
|
||||
// Member line: match by subLine index
|
||||
if (isMemberSel) {
|
||||
if (!m_meta[ln].isMemberLine || m_meta[ln].subLine != memberSubLine)
|
||||
continue;
|
||||
} else if (m_meta[ln].isMemberLine) {
|
||||
continue;
|
||||
}
|
||||
m_sci->markerAdd(ln, M_SELECTED);
|
||||
@@ -1127,7 +1135,8 @@ void RcxEditor::applyHoverHighlight() {
|
||||
if (prevId != 0) {
|
||||
// Check if old hovered line was a single-line highlight (footer or array element)
|
||||
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
||||
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement));
|
||||
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement
|
||||
|| m_meta[prevLine].isMemberLine));
|
||||
if (prevSingleLine) {
|
||||
m_sci->markerDelete(prevLine, M_HOVER);
|
||||
} else {
|
||||
@@ -1143,11 +1152,13 @@ void RcxEditor::applyHoverHighlight() {
|
||||
if (!m_hoverInside) return;
|
||||
if (m_hoveredNodeId == 0) return;
|
||||
|
||||
// Footer and array elements highlight only the specific line
|
||||
// Footer, array elements, and member lines highlight only the specific line
|
||||
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].isArrayElement);
|
||||
bool hoveringMember = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].isMemberLine);
|
||||
|
||||
// Check if the hovered item is already selected (using appropriate ID)
|
||||
uint64_t checkId;
|
||||
@@ -1155,12 +1166,14 @@ void RcxEditor::applyHoverHighlight() {
|
||||
checkId = m_hoveredNodeId | kFooterIdBit;
|
||||
else if (hoveringArrayElem)
|
||||
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
||||
else if (hoveringMember)
|
||||
checkId = makeMemberSelId(m_hoveredNodeId, m_meta[m_hoveredLine].subLine);
|
||||
else
|
||||
checkId = m_hoveredNodeId;
|
||||
if (m_currentSelIds.contains(checkId)) return;
|
||||
|
||||
if (hoveringFooter || hoveringArrayElem) {
|
||||
// Single-line highlight for footers and array elements
|
||||
if (hoveringFooter || hoveringArrayElem || hoveringMember) {
|
||||
// Single-line highlight for footers, array elements, and member lines
|
||||
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||
} else {
|
||||
// Non-footer, non-array-element: highlight all lines for this node
|
||||
@@ -1374,15 +1387,6 @@ void RcxEditor::applyCommandRowPills() {
|
||||
if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart))
|
||||
fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1);
|
||||
}
|
||||
// Dim all " · " separators
|
||||
int searchFrom = 0;
|
||||
while (true) {
|
||||
int tag = t.indexOf(QStringLiteral(" \u00B7"), searchFrom);
|
||||
if (tag < 0) break;
|
||||
fillIndicatorCols(IND_HEX_DIM, line, tag, tag + 3);
|
||||
searchFrom = tag + 3;
|
||||
}
|
||||
|
||||
// Dim base address to match source/struct grey
|
||||
ColumnSpan addrSpan = commandRowAddrSpan(t);
|
||||
if (addrSpan.valid)
|
||||
@@ -1615,6 +1619,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
if (!s.valid && t == EditTarget::Name)
|
||||
s = headerNameSpan(*lm, lineText);
|
||||
|
||||
// Member lines: override Name/Value spans
|
||||
if (!s.valid && lm->isMemberLine) {
|
||||
if (t == EditTarget::Name) s = memberNameSpanFor(*lm, lineText);
|
||||
if (t == EditTarget::Value) s = memberValueSpanFor(*lm, lineText);
|
||||
}
|
||||
|
||||
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true);
|
||||
if (lineTextOut) *lineTextOut = lineText;
|
||||
return out.valid;
|
||||
@@ -1728,6 +1738,12 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
if (!ns.valid)
|
||||
ns = headerNameSpan(lm, lineText);
|
||||
|
||||
// Member lines: use name/value spans from line text (no type span)
|
||||
if (lm.isMemberLine) {
|
||||
ns = memberNameSpanFor(lm, lineText);
|
||||
vs = memberValueSpanFor(lm, lineText);
|
||||
}
|
||||
|
||||
if (inSpan(ts)) outTarget = EditTarget::Type;
|
||||
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
||||
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
||||
@@ -2686,6 +2702,8 @@ void RcxEditor::updateEditableIndicators(int line) {
|
||||
checkId = lm->nodeId | kFooterIdBit;
|
||||
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
||||
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
||||
else if (lm->isMemberLine && lm->subLine >= 0)
|
||||
checkId = makeMemberSelId(lm->nodeId, lm->subLine);
|
||||
else
|
||||
checkId = lm->nodeId;
|
||||
return m_currentSelIds.contains(checkId);
|
||||
|
||||
@@ -61,6 +61,8 @@ public:
|
||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
||||
}
|
||||
|
||||
void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); }
|
||||
|
||||
// Saved sources for quick-switch in source picker
|
||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
{"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"},
|
||||
|
||||
{"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"191","kind":"Hex8","name":"ExecuteOptions","offset":0,"parentId":"190"},
|
||||
{"id":"191","kind":"Struct","name":"","offset":0,"parentId":"190","refId":"0","collapsed":false},
|
||||
{"id":"192","kind":"UInt8","name":"ExecuteDisable","offset":0,"parentId":"191"},
|
||||
{"id":"193","kind":"Hex8","name":"ExecuteDisable:1 ExecuteEnable:1 DisableThunkEmulation:1 Permanent:1 ExecuteDispatchEnable:1 ImageDispatchEnable:1 DisableExceptionChainValidation:1 Spare:1","offset":0,"parentId":"191"},
|
||||
{"id":"194","kind":"UInt8","name":"ExecuteOptions","offset":0,"parentId":"190"},
|
||||
|
||||
{"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"},
|
||||
|
||||
@@ -121,15 +121,8 @@ QString fmtDouble(double v) {
|
||||
}
|
||||
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
||||
|
||||
QString fmtPointer32(uint32_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
|
||||
QString fmtPointer64(uint64_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
QString fmtPointer32(uint32_t v) { return hexVal(v); }
|
||||
QString fmtPointer64(uint64_t v) { return hexVal(v); }
|
||||
|
||||
// ── Indentation ──
|
||||
|
||||
@@ -148,11 +141,11 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
|
||||
// ── Struct type name (for width calculation) ──
|
||||
|
||||
QString structTypeName(const Node& node) {
|
||||
// Full type string: "struct TypeName", "union TypeName", "class TypeName", etc.
|
||||
QString base = node.resolvedClassKeyword();
|
||||
// Named types: just the type name (e.g. "_LIST_ENTRY")
|
||||
// Anonymous: just the keyword (e.g. "union", "struct")
|
||||
if (!node.structTypeName.isEmpty())
|
||||
return base + QStringLiteral(" ") + node.structTypeName;
|
||||
return base;
|
||||
return node.structTypeName;
|
||||
return node.resolvedClassKeyword();
|
||||
}
|
||||
|
||||
// ── Struct header / footer ──
|
||||
@@ -710,4 +703,27 @@ QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW)
|
||||
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
|
||||
}
|
||||
|
||||
// ── Bitfield member formatting ──
|
||||
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth) {
|
||||
uint64_t container = 0;
|
||||
switch (containerKind) {
|
||||
case NodeKind::Hex8: container = prov.readU8(addr); break;
|
||||
case NodeKind::Hex16: container = prov.readU16(addr); break;
|
||||
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
||||
default: container = prov.readU64(addr); break;
|
||||
}
|
||||
if (bitWidth >= 64) return container >> bitOffset;
|
||||
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
||||
}
|
||||
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW)
|
||||
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
|
||||
}
|
||||
|
||||
} // namespace rcx::fmt
|
||||
|
||||
@@ -115,6 +115,24 @@ bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* er
|
||||
while (i < children.size()) {
|
||||
const Node& child = tree.nodes[children[i]];
|
||||
|
||||
// Bitfield container: export as hex node (ReClassEx has no bitfield concept)
|
||||
if (child.kind == NodeKind::Struct
|
||||
&& child.resolvedClassKeyword() == QStringLiteral("bitfield")) {
|
||||
int sz = child.byteSize();
|
||||
if (sz <= 0) sz = 4;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
NodeKind hexKind = (sz <= 1) ? NodeKind::Hex8 : (sz <= 2) ? NodeKind::Hex16
|
||||
: (sz <= 4) ? NodeKind::Hex32 : NodeKind::Hex64;
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(hexKind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(sz));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QStringLiteral("bitfield"));
|
||||
xml.writeEndElement();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||
if (isHexNode(child.kind)) {
|
||||
int runStart = child.offset;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QHash>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QDebug>
|
||||
|
||||
// ── RawPDB headers ──
|
||||
#include "PDB.h"
|
||||
@@ -415,6 +416,7 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
||||
|
||||
auto maximumSize = rec->header.size - sizeof(uint16_t);
|
||||
QSet<QPair<int,int>> bitfieldSlots;
|
||||
QHash<QPair<int,int>, uint64_t> bitfieldNodeIds;
|
||||
|
||||
for (size_t i = 0; i < maximumSize; ) {
|
||||
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
||||
@@ -440,7 +442,7 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
||||
if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) {
|
||||
uint32_t underlying = typeRec->data.LF_BITFIELD.type;
|
||||
uint8_t bitLen = typeRec->data.LF_BITFIELD.length;
|
||||
(void)bitLen;
|
||||
uint8_t bitPos = typeRec->data.LF_BITFIELD.position;
|
||||
|
||||
// Determine slot size from underlying type
|
||||
uint64_t slotSize = 4;
|
||||
@@ -452,12 +454,26 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
||||
auto key = qMakePair((int)offset, (int)slotSize);
|
||||
if (!bitfieldSlots.contains(key)) {
|
||||
bitfieldSlots.insert(key);
|
||||
// Create bitfield container node
|
||||
Node n;
|
||||
n.kind = hexForSize(slotSize);
|
||||
n.name = qname;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = hexForSize(slotSize);
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
tree.addNode(n);
|
||||
n.collapsed = false;
|
||||
int idx = tree.addNode(n);
|
||||
bitfieldNodeIds[key] = tree.nodes[idx].id;
|
||||
}
|
||||
// Add this member to the bitfield container
|
||||
uint64_t bfNodeId = bitfieldNodeIds[key];
|
||||
int bfIdx = tree.indexOfId(bfNodeId);
|
||||
if (bfIdx >= 0) {
|
||||
BitfieldMember bm;
|
||||
bm.name = qname;
|
||||
bm.bitOffset = bitPos;
|
||||
bm.bitWidth = bitLen;
|
||||
tree.nodes[bfIdx].bitfieldMembers.append(bm);
|
||||
}
|
||||
} else {
|
||||
importMemberType(memberType, offset, qname, parentId);
|
||||
@@ -641,7 +657,21 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
uint32_t resolved = findUdtDefinitionIndex(pointeeRec->header.kind, typeName);
|
||||
if (resolved != 0) defIndex = resolved;
|
||||
}
|
||||
n.refId = importUDT(defIndex);
|
||||
// Skip anonymous pointer targets — they'd create root orphans
|
||||
const char* ptName = nullptr;
|
||||
const auto* defRec2 = tt->get(defIndex);
|
||||
if (defRec2) {
|
||||
if (defRec2->header.kind == TRK::LF_UNION)
|
||||
ptName = leafName(defRec2->data.LF_UNION.data,
|
||||
unionLeafKind(defRec2->data.LF_UNION.data));
|
||||
else if (defRec2->header.kind == TRK::LF_STRUCTURE ||
|
||||
defRec2->header.kind == TRK::LF_CLASS)
|
||||
ptName = leafName(defRec2->data.LF_CLASS.data,
|
||||
defRec2->data.LF_CLASS.lfEasy.kind);
|
||||
}
|
||||
bool isAnonTarget = !ptName || ptName[0] == '<' || ptName[0] == '\0';
|
||||
if (!isAnonTarget)
|
||||
n.refId = importUDT(defIndex);
|
||||
} else if (pointeeRec->header.kind == TRK::LF_PROCEDURE ||
|
||||
pointeeRec->header.kind == TRK::LF_MFUNCTION) {
|
||||
n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64;
|
||||
@@ -676,8 +706,6 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
if (resolved != 0) defIndex = resolved;
|
||||
}
|
||||
|
||||
uint64_t refId = importUDT(defIndex);
|
||||
|
||||
const char* typeName = nullptr;
|
||||
bool isUnion = (rec->header.kind == TRK::LF_UNION);
|
||||
if (isUnion)
|
||||
@@ -685,6 +713,38 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
else
|
||||
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||
|
||||
// Anonymous types: inline fields directly instead of creating root orphan
|
||||
bool isAnonymous = !typeName || typeName[0] == '<' || typeName[0] == '\0';
|
||||
if (isAnonymous) {
|
||||
// Resolve to definition if needed
|
||||
const auto* defRec = tt->get(defIndex);
|
||||
uint32_t fieldListIdx = 0;
|
||||
if (defRec) {
|
||||
if (defRec->header.kind == TRK::LF_UNION)
|
||||
fieldListIdx = defRec->data.LF_UNION.field;
|
||||
else if (defRec->header.kind == TRK::LF_STRUCTURE ||
|
||||
defRec->header.kind == TRK::LF_CLASS)
|
||||
fieldListIdx = defRec->data.LF_CLASS.field;
|
||||
}
|
||||
if (fieldListIdx != 0) {
|
||||
// Create inline container (no refId, no root orphan)
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = name;
|
||||
n.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct");
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.collapsed = true;
|
||||
int idx = tree.addNode(n);
|
||||
uint64_t inlineId = tree.nodes[idx].id;
|
||||
importFieldList(fieldListIdx, inlineId);
|
||||
break;
|
||||
}
|
||||
// Fallthrough if no field list
|
||||
}
|
||||
|
||||
uint64_t refId = importUDT(defIndex);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = name;
|
||||
@@ -806,16 +866,21 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
|
||||
case TRK::LF_BITFIELD: {
|
||||
uint32_t underlying = rec->data.LF_BITFIELD.type;
|
||||
uint8_t bitLen = rec->data.LF_BITFIELD.length;
|
||||
uint8_t bitPos = rec->data.LF_BITFIELD.position;
|
||||
uint64_t slotSize = 4;
|
||||
if (underlying < tt->firstIndex()) {
|
||||
NodeKind k = mapPrimitiveType(underlying);
|
||||
slotSize = sizeForKind(k);
|
||||
}
|
||||
Node n;
|
||||
n.kind = hexForSize(slotSize);
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = hexForSize(slotSize);
|
||||
n.name = name;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.bitfieldMembers.append({name, bitPos, bitLen});
|
||||
tree.addNode(n);
|
||||
break;
|
||||
}
|
||||
@@ -944,6 +1009,12 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
|
||||
result.append(info);
|
||||
}
|
||||
|
||||
int enumCount = 0;
|
||||
for (const auto& r : result)
|
||||
if (r.isEnum) enumCount++;
|
||||
qDebug() << "[PDB] enumeratePdbTypes:" << result.size() << "types,"
|
||||
<< enumCount << "enums";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -960,19 +1031,34 @@ NodeTree importPdbSelected(const QString& pdbPath,
|
||||
ctx.tt = pdb.typeTable;
|
||||
|
||||
int total = typeIndices.size();
|
||||
int enumDispatched = 0, enumCreated = 0;
|
||||
for (int i = 0; i < total; i++) {
|
||||
uint32_t ti = typeIndices[i];
|
||||
const auto* rec = pdb.typeTable->get(ti);
|
||||
if (rec && rec->header.kind == TRK::LF_ENUM)
|
||||
ctx.importEnum(ti);
|
||||
else
|
||||
if (rec && rec->header.kind == TRK::LF_ENUM) {
|
||||
enumDispatched++;
|
||||
uint64_t id = ctx.importEnum(ti);
|
||||
if (id != 0) enumCreated++;
|
||||
else qDebug() << "[PDB] importEnum FAILED for typeIndex" << ti;
|
||||
} else {
|
||||
ctx.importUDT(ti);
|
||||
}
|
||||
if (progressCb && !progressCb(i + 1, total)) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
|
||||
return ctx.tree; // return partial result
|
||||
}
|
||||
}
|
||||
|
||||
// Count enum nodes in tree
|
||||
int enumNodes = 0;
|
||||
for (const auto& n : ctx.tree.nodes)
|
||||
if (n.classKeyword == QLatin1String("enum")) enumNodes++;
|
||||
qDebug() << "[PDB] importPdbSelected:" << total << "types,"
|
||||
<< enumDispatched << "enum dispatches,"
|
||||
<< enumCreated << "enum created,"
|
||||
<< enumNodes << "enum nodes in tree,"
|
||||
<< ctx.tree.nodes.size() << "total nodes";
|
||||
|
||||
if (ctx.tree.nodes.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No types imported");
|
||||
}
|
||||
|
||||
@@ -894,20 +894,40 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitfield grouping: emit a single hex node covering consecutive bitfields ──
|
||||
// ── Bitfield grouping: emit a bitfield container with named members ──
|
||||
|
||||
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, int totalBits) {
|
||||
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset,
|
||||
const QVector<ParsedField>& fields,
|
||||
int startIdx, int endIdx) {
|
||||
int totalBits = 0;
|
||||
for (int i = startIdx; i < endIdx; i++)
|
||||
totalBits += fields[i].bitfieldWidth;
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
// Round up to nearest power-of-2 hex node
|
||||
NodeKind hexKind;
|
||||
if (bytes <= 1) hexKind = NodeKind::Hex8;
|
||||
else if (bytes <= 2) hexKind = NodeKind::Hex16;
|
||||
else if (bytes <= 4) hexKind = NodeKind::Hex32;
|
||||
else hexKind = NodeKind::Hex64;
|
||||
NodeKind containerKind;
|
||||
if (bytes <= 1) containerKind = NodeKind::Hex8;
|
||||
else if (bytes <= 2) containerKind = NodeKind::Hex16;
|
||||
else if (bytes <= 4) containerKind = NodeKind::Hex32;
|
||||
else containerKind = NodeKind::Hex64;
|
||||
|
||||
Node n;
|
||||
n.kind = hexKind;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = containerKind;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.collapsed = false;
|
||||
|
||||
// Populate bitfield members with computed bit offsets
|
||||
uint8_t bitOffset = 0;
|
||||
for (int i = startIdx; i < endIdx; i++) {
|
||||
BitfieldMember bm;
|
||||
bm.name = fields[i].name;
|
||||
bm.bitOffset = bitOffset;
|
||||
bm.bitWidth = (uint8_t)fields[i].bitfieldWidth;
|
||||
n.bitfieldMembers.append(bm);
|
||||
bitOffset += bm.bitWidth;
|
||||
}
|
||||
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
@@ -929,13 +949,14 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
for (int fi = 0; fi < fields.size(); fi++) {
|
||||
const auto& field = fields[fi];
|
||||
|
||||
// Bitfield group: consume consecutive bitfields, emit single hex node
|
||||
// Bitfield group: consume consecutive bitfields, emit bitfield container
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
groupOffset = computedOffset;
|
||||
int startIdx = fi;
|
||||
int totalBits = 0;
|
||||
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
|
||||
totalBits += fields[fi].bitfieldWidth;
|
||||
@@ -943,7 +964,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
}
|
||||
fi--; // compensate for outer loop increment
|
||||
if (totalBits > 0)
|
||||
emitBitfieldGroup(ctx.tree, parentId, groupOffset, totalBits);
|
||||
emitBitfieldGroup(ctx.tree, parentId, groupOffset,
|
||||
fields, startIdx, fi + 1);
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
int nodeSize = (bytes <= 1) ? 1 : (bytes <= 2) ? 2 : (bytes <= 4) ? 4 : 8;
|
||||
computedOffset = groupOffset + nodeSize;
|
||||
|
||||
385
src/main.cpp
385
src/main.cpp
@@ -312,9 +312,13 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tree view items — use theme.hover for selection instead of blue
|
||||
// Item views — visible hover + themed selection (Fusion's hover is invisible on dark bg)
|
||||
if (element == CE_ItemViewItem) {
|
||||
if (auto* vi = qstyleoption_cast<const QStyleOptionViewItem*>(opt)) {
|
||||
bool hovered = vi->state & State_MouseOver;
|
||||
bool selected = vi->state & State_Selected;
|
||||
if (hovered && !selected)
|
||||
p->fillRect(vi->rect, vi->palette.color(QPalette::Mid));
|
||||
QStyleOptionViewItem patched = *vi;
|
||||
patched.palette.setColor(QPalette::Highlight,
|
||||
vi->palette.color(QPalette::Mid)); // theme.hover
|
||||
@@ -454,7 +458,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
|
||||
// Start MCP bridge
|
||||
m_mcp = new McpBridge(this, this);
|
||||
if (QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool())
|
||||
if (QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool())
|
||||
m_mcp->start();
|
||||
|
||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
||||
@@ -503,22 +507,19 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||
file->addSeparator();
|
||||
m_sourceMenu = file->addMenu("Current Tab So&urce");
|
||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
||||
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||
Qt5Qt6AddAction(file, "Import &PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb);
|
||||
auto* importMenu = file->addMenu("&Import");
|
||||
Qt5Qt6AddAction(importMenu, "From &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||
Qt5Qt6AddAction(importMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||
Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb);
|
||||
auto* exportMenu = file->addMenu("E&xport");
|
||||
Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp);
|
||||
Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
// Examples submenu — scan once at init
|
||||
{
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||
if (!rcxFiles.isEmpty()) {
|
||||
auto* examples = file->addMenu("&Examples");
|
||||
auto* examples = file->addMenu("E&xamples");
|
||||
for (const QString& fn : rcxFiles) {
|
||||
QString fullPath = exDir.absoluteFilePath(fn);
|
||||
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
|
||||
@@ -526,10 +527,7 @@ void MainWindow::createMenus() {
|
||||
}
|
||||
}
|
||||
file->addSeparator();
|
||||
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||
Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
||||
|
||||
@@ -537,13 +535,14 @@ void MainWindow::createMenus() {
|
||||
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
||||
Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
||||
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
||||
edit->addSeparator();
|
||||
Qt5Qt6AddAction(edit, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||
|
||||
// View
|
||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
||||
Qt5Qt6AddAction(view, "&Unsplit", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
view->addSeparator();
|
||||
m_sourceMenu = view->addMenu("&Data Source");
|
||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
||||
view->addSeparator();
|
||||
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
||||
auto* fontGroup = new QActionGroup(this);
|
||||
@@ -590,9 +589,28 @@ void MainWindow::createMenus() {
|
||||
tab.ctrl->setCompactColumns(checked);
|
||||
});
|
||||
|
||||
auto* actRelOfs = view->addAction("R&elative Offsets");
|
||||
actRelOfs->setCheckable(true);
|
||||
actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
|
||||
connect(actRelOfs, &QAction::triggered, this, [this](bool checked) {
|
||||
QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked);
|
||||
for (auto& tab : m_tabs)
|
||||
for (auto& pane : tab.panes)
|
||||
pane.editor->setRelativeOffsets(checked);
|
||||
});
|
||||
|
||||
view->addSeparator();
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
|
||||
// Tools
|
||||
auto* tools = m_titleBar->menuBar()->addMenu("&Tools");
|
||||
Qt5Qt6AddAction(tools, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||
tools->addSeparator();
|
||||
const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||
m_mcpAction = Qt5Qt6AddAction(tools, mcpName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||
tools->addSeparator();
|
||||
Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||
|
||||
// Plugins
|
||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
||||
@@ -716,6 +734,80 @@ protected:
|
||||
void leaveEvent(QEvent*) override { update(); }
|
||||
};
|
||||
|
||||
// ── Shimmer label — gradient text sweep for MCP activity ──
|
||||
class ShimmerLabel : public QWidget {
|
||||
public:
|
||||
explicit ShimmerLabel(QWidget* parent = nullptr) : QWidget(parent) {
|
||||
m_timer.setInterval(30);
|
||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||
m_phase += 0.012f;
|
||||
if (m_phase > 1.0f) m_phase -= 1.0f;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
void setText(const QString& t) { m_text = t; update(); }
|
||||
QString text() const { return m_text; }
|
||||
|
||||
void setShimmerActive(bool on) {
|
||||
if (m_shimmer == on) return;
|
||||
m_shimmer = on;
|
||||
if (on) { m_phase = 0.0f; m_timer.start(); }
|
||||
else { m_timer.stop(); }
|
||||
update();
|
||||
}
|
||||
bool shimmerActive() const { return m_shimmer; }
|
||||
|
||||
void setAlignment(Qt::Alignment a) { m_align = a; update(); }
|
||||
|
||||
// Colours configurable from theme
|
||||
QColor colBase; // dim text (normal)
|
||||
QColor colBright; // highlight sweep
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
if (m_text.isEmpty()) return;
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::TextAntialiasing);
|
||||
p.setFont(font());
|
||||
|
||||
QRect r = contentsRect();
|
||||
|
||||
if (!m_shimmer) {
|
||||
QColor c = colBase.isValid() ? colBase
|
||||
: palette().color(QPalette::WindowText);
|
||||
p.setPen(c);
|
||||
p.drawText(r, m_align, m_text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shimmer: sweeping glow band behind text + bright text
|
||||
QColor bright = colBright.isValid() ? colBright : QColor(255, 200, 80);
|
||||
|
||||
// 1. Sweeping glow band (semi-transparent background highlight)
|
||||
qreal bandW = width() * 0.20;
|
||||
qreal bandCenter = -bandW + (width() + 2 * bandW) * m_phase;
|
||||
QLinearGradient bgGrad(bandCenter - bandW, 0, bandCenter + bandW, 0);
|
||||
QColor glow = bright;
|
||||
glow.setAlpha(35);
|
||||
bgGrad.setColorAt(0.0, Qt::transparent);
|
||||
bgGrad.setColorAt(0.5, glow);
|
||||
bgGrad.setColorAt(1.0, Qt::transparent);
|
||||
p.fillRect(rect(), QBrush(bgGrad));
|
||||
|
||||
// 2. Text in bright color
|
||||
p.setPen(bright);
|
||||
p.drawText(r, m_align, m_text);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_text;
|
||||
bool m_shimmer = false;
|
||||
float m_phase = 0.0f;
|
||||
Qt::Alignment m_align = Qt::AlignLeft | Qt::AlignVCenter;
|
||||
QTimer m_timer;
|
||||
};
|
||||
|
||||
// ── Borderless status bar with manual child layout ──
|
||||
// QStatusBarLayout hardcodes 2px margins that can't be overridden.
|
||||
// We bypass it entirely: children are placed manually in resizeEvent,
|
||||
@@ -723,8 +815,8 @@ protected:
|
||||
// children and call manualLayout() to position them.
|
||||
class FlatStatusBar : public QStatusBar {
|
||||
public:
|
||||
QWidget* tabRow = nullptr; // set by createStatusBar
|
||||
QLabel* label = nullptr; // set by createStatusBar
|
||||
QWidget* tabRow = nullptr; // set by createStatusBar
|
||||
ShimmerLabel* label = nullptr; // set by createStatusBar
|
||||
|
||||
void setDividerColor(const QColor& c) { m_div = c; update(); }
|
||||
void setTopLineColor(const QColor& c) { m_top = c; update(); }
|
||||
@@ -802,7 +894,8 @@ void MainWindow::createStatusBar() {
|
||||
auto* sb = new FlatStatusBar;
|
||||
setStatusBar(sb);
|
||||
|
||||
m_statusLabel = new QLabel("Ready", sb);
|
||||
m_statusLabel = new ShimmerLabel(sb);
|
||||
m_statusLabel->setText("");
|
||||
m_statusLabel->setContentsMargins(0, 0, 0, 0);
|
||||
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
||||
|
||||
@@ -865,10 +958,42 @@ void MainWindow::createStatusBar() {
|
||||
};
|
||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnReclass));
|
||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnRendered));
|
||||
|
||||
m_statusLabel->colBase = t.textDim;
|
||||
m_statusLabel->colBright = t.indHoverSpan;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MainWindow::setAppStatus(const QString& text) {
|
||||
m_appStatus = text;
|
||||
if (!m_mcpBusy) {
|
||||
m_statusLabel->setText(text);
|
||||
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();
|
||||
m_mcpBusy = true;
|
||||
m_statusLabel->setText(text);
|
||||
m_statusLabel->setShimmerActive(true);
|
||||
}
|
||||
|
||||
void MainWindow::clearMcpStatus() {
|
||||
// Delay the clear so the shimmer stays visible for at least 750ms
|
||||
if (!m_mcpClearTimer) {
|
||||
m_mcpClearTimer = new QTimer(this);
|
||||
m_mcpClearTimer->setSingleShot(true);
|
||||
connect(m_mcpClearTimer, &QTimer::timeout, this, [this]() {
|
||||
m_mcpBusy = false;
|
||||
m_statusLabel->setText(m_appStatus);
|
||||
m_statusLabel->setShimmerActive(false);
|
||||
});
|
||||
}
|
||||
m_mcpClearTimer->start(750);
|
||||
}
|
||||
|
||||
void MainWindow::styleTabCloseButtons() {
|
||||
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
|
||||
@@ -911,6 +1036,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
|
||||
// Create editor via controller (parent = tabWidget for ownership)
|
||||
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
|
||||
pane.editor->setRelativeOffsets(
|
||||
QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool());
|
||||
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
|
||||
|
||||
// Create per-pane rendered C++ view
|
||||
@@ -1033,19 +1160,17 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
||||
auto* ap = findActiveSplitPane();
|
||||
if (ap && ap->viewMode == VM_Rendered)
|
||||
m_statusLabel->setText(
|
||||
setAppStatus(
|
||||
QString("Rendered: %1 %2")
|
||||
.arg(kindToString(node.kind))
|
||||
.arg(node.name));
|
||||
else
|
||||
m_statusLabel->setText(
|
||||
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()));
|
||||
} else {
|
||||
m_statusLabel->setText("Ready");
|
||||
}
|
||||
// Update all rendered panes on selection change
|
||||
auto it = m_tabs.find(sub);
|
||||
@@ -1054,10 +1179,8 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
});
|
||||
connect(ctrl, &RcxController::selectionChanged,
|
||||
this, [this](int count) {
|
||||
if (count == 0)
|
||||
m_statusLabel->setText("Ready");
|
||||
else if (count > 1)
|
||||
m_statusLabel->setText(QString("%1 nodes selected").arg(count));
|
||||
if (count > 1)
|
||||
setAppStatus(QString("%1 nodes selected").arg(count));
|
||||
});
|
||||
|
||||
// Update rendered panes and workspace on document changes and undo/redo
|
||||
@@ -1417,7 +1540,9 @@ void MainWindow::removeNode() {
|
||||
QSet<uint64_t> ids = ctrl->selectedIds();
|
||||
QVector<int> indices;
|
||||
for (uint64_t id : ids) {
|
||||
int idx = ctrl->document()->tree.indexOfId(id & ~kFooterIdBit);
|
||||
int idx = ctrl->document()->tree.indexOfId(
|
||||
id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask));
|
||||
if (idx >= 0) indices.append(idx);
|
||||
}
|
||||
if (indices.size() > 1)
|
||||
@@ -1522,11 +1647,11 @@ void MainWindow::toggleMcp() {
|
||||
if (m_mcp->isRunning()) {
|
||||
m_mcp->stop();
|
||||
m_mcpAction->setText("Start &MCP Server");
|
||||
m_statusLabel->setText("MCP server stopped");
|
||||
setAppStatus("MCP server stopped");
|
||||
} else {
|
||||
m_mcp->start();
|
||||
m_mcpAction->setText("Stop &MCP Server");
|
||||
m_statusLabel->setText("MCP server listening on pipe: ReclassMcpBridge");
|
||||
setAppStatus("MCP server listening on pipe: ReclassMcpBridge");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1541,15 +1666,21 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
// Update border overlay color
|
||||
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||
|
||||
// MDI area tabs
|
||||
// MDI area tabs — text color + height handled by MenuBarStyle QProxyStyle
|
||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 0px 16px; border: none; height: 24px;"
|
||||
" background: %1; padding: 0px 16px; border: none;"
|
||||
"}"
|
||||
"QTabBar::tab:selected { color: %3; background: %4; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
||||
theme.backgroundAlt.name(), theme.hover.name()));
|
||||
"QTabBar::tab:selected { background: %2; }"
|
||||
"QTabBar::tab:hover { background: %3; }")
|
||||
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.name()));
|
||||
|
||||
// Dim MDI tab text via palette (Fusion reads WindowText, not CSS color:)
|
||||
if (auto* tabBar = m_mdiArea->findChild<QTabBar*>()) {
|
||||
QPalette tp = tabBar->palette();
|
||||
tp.setColor(QPalette::WindowText, theme.textDim);
|
||||
tabBar->setPalette(tp);
|
||||
}
|
||||
|
||||
// Re-style ✕ close buttons on MDI tabs
|
||||
styleTabCloseButtons();
|
||||
@@ -1594,6 +1725,12 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
tp.setColor(QPalette::HighlightedText, theme.text);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
if (m_workspaceSearch) {
|
||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: none;"
|
||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
||||
.arg(theme.background.name(), theme.textDim.name(), theme.border.name()));
|
||||
}
|
||||
|
||||
// Dock titlebar: restyle via palette + close button
|
||||
if (m_dockTitleLabel) {
|
||||
@@ -1665,7 +1802,7 @@ void MainWindow::showOptionsDialog() {
|
||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
|
||||
OptionsDialog dlg(current, this);
|
||||
@@ -1878,7 +2015,8 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
|
||||
QSet<uint64_t> selIds = tab.ctrl->selectedIds();
|
||||
if (selIds.size() >= 1) {
|
||||
uint64_t selId = *selIds.begin();
|
||||
selId &= ~kFooterIdBit;
|
||||
selId &= ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||
| kMemberBit | kMemberSubMask);
|
||||
rootId = findRootStructForNode(tab.doc->tree, selId);
|
||||
}
|
||||
|
||||
@@ -1941,7 +2079,7 @@ void MainWindow::exportCpp() {
|
||||
return;
|
||||
}
|
||||
file.write(text.toUtf8());
|
||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
||||
setAppStatus("Exported to " + QFileInfo(path).fileName());
|
||||
}
|
||||
|
||||
// ── Export ReClass XML ──
|
||||
@@ -1965,7 +2103,7 @@ void MainWindow::exportReclassXmlAction() {
|
||||
for (const auto& n : tab->doc->tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
|
||||
m_statusLabel->setText(QStringLiteral("Exported %1 classes to %2")
|
||||
setAppStatus(QStringLiteral("Exported %1 classes to %2")
|
||||
.arg(classCount).arg(QFileInfo(path).fileName()));
|
||||
}
|
||||
|
||||
@@ -1996,7 +2134,7 @@ void MainWindow::importReclassXml() {
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
}
|
||||
|
||||
@@ -2046,7 +2184,7 @@ void MainWindow::importFromSource() {
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||
}
|
||||
|
||||
// ── Import PDB ──
|
||||
@@ -2096,7 +2234,7 @@ void MainWindow::importPdb() {
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(pdbPath).fileName()));
|
||||
}
|
||||
|
||||
@@ -2108,30 +2246,91 @@ void MainWindow::showTypeAliasesDialog() {
|
||||
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle("Type Aliases");
|
||||
dlg.resize(500, 400);
|
||||
dlg.resize(400, 380);
|
||||
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
|
||||
// Preset buttons (stdint + Windows only, no redundant Reset)
|
||||
auto* presetRow = new QHBoxLayout;
|
||||
auto* btnStdint = new QPushButton("stdint (C99)", &dlg);
|
||||
auto* btnWindows = new QPushButton("Windows (basetsd.h)", &dlg);
|
||||
presetRow->addWidget(btnStdint);
|
||||
presetRow->addWidget(btnWindows);
|
||||
presetRow->addStretch();
|
||||
layout->addLayout(presetRow);
|
||||
|
||||
auto* table = new QTableWidget(&dlg);
|
||||
table->setColumnCount(2);
|
||||
table->setHorizontalHeaderLabels({"NodeKind", "Alias (C type)"});
|
||||
table->horizontalHeader()->setVisible(false);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
|
||||
// Populate with all NodeKind entries
|
||||
int rowCount = static_cast<int>(std::size(kKindMeta));
|
||||
table->setRowCount(rowCount);
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
const auto& meta = kKindMeta[i];
|
||||
// Skip types that nobody aliases (Vec, Mat, Struct, Array)
|
||||
auto shouldSkip = [](NodeKind k) {
|
||||
return k == NodeKind::Vec2 || k == NodeKind::Vec3
|
||||
|| k == NodeKind::Vec4 || k == NodeKind::Mat4x4
|
||||
|| k == NodeKind::Struct || k == NodeKind::Array;
|
||||
};
|
||||
|
||||
// Build filtered row→meta index mapping
|
||||
QVector<int> rowMap;
|
||||
int totalMeta = static_cast<int>(std::size(kKindMeta));
|
||||
for (int i = 0; i < totalMeta; i++)
|
||||
if (!shouldSkip(kKindMeta[i].kind)) rowMap.append(i);
|
||||
|
||||
table->setRowCount(rowMap.size());
|
||||
for (int row = 0; row < rowMap.size(); row++) {
|
||||
const auto& meta = kKindMeta[rowMap[row]];
|
||||
auto* kindItem = new QTableWidgetItem(QString::fromLatin1(meta.name));
|
||||
kindItem->setFlags(kindItem->flags() & ~Qt::ItemIsEditable);
|
||||
table->setItem(i, 0, kindItem);
|
||||
table->setItem(row, 0, kindItem);
|
||||
|
||||
QString alias = tab->doc->typeAliases.value(meta.kind);
|
||||
table->setItem(i, 1, new QTableWidgetItem(alias));
|
||||
table->setItem(row, 1, new QTableWidgetItem(alias));
|
||||
}
|
||||
|
||||
// stdint preset: actual typeName values from kKindMeta
|
||||
static QHash<NodeKind, QString> kStdintPreset;
|
||||
if (kStdintPreset.isEmpty()) {
|
||||
for (const auto& m : kKindMeta)
|
||||
kStdintPreset[m.kind] = QString::fromLatin1(m.typeName);
|
||||
}
|
||||
|
||||
// Windows (basetsd.h) preset mapping
|
||||
static const QHash<NodeKind, QString> kWindowsPreset = {
|
||||
{NodeKind::Int8, QStringLiteral("CHAR")},
|
||||
{NodeKind::Int16, QStringLiteral("SHORT")},
|
||||
{NodeKind::Int32, QStringLiteral("LONG")},
|
||||
{NodeKind::Int64, QStringLiteral("LONGLONG")},
|
||||
{NodeKind::UInt8, QStringLiteral("UCHAR")},
|
||||
{NodeKind::UInt16, QStringLiteral("USHORT")},
|
||||
{NodeKind::UInt32, QStringLiteral("ULONG")},
|
||||
{NodeKind::UInt64, QStringLiteral("ULONGLONG")},
|
||||
{NodeKind::Float, QStringLiteral("FLOAT")},
|
||||
{NodeKind::Double, QStringLiteral("DOUBLE")},
|
||||
{NodeKind::Bool, QStringLiteral("BOOLEAN")},
|
||||
{NodeKind::Pointer32, QStringLiteral("ULONG")},
|
||||
{NodeKind::Pointer64, QStringLiteral("ULONG_PTR")},
|
||||
{NodeKind::FuncPtr32, QStringLiteral("ULONG")},
|
||||
{NodeKind::FuncPtr64, QStringLiteral("ULONG_PTR")},
|
||||
{NodeKind::Hex8, QStringLiteral("BYTE")},
|
||||
{NodeKind::Hex16, QStringLiteral("WORD")},
|
||||
{NodeKind::Hex32, QStringLiteral("DWORD")},
|
||||
{NodeKind::Hex64, QStringLiteral("DWORD64")},
|
||||
{NodeKind::UTF8, QStringLiteral("CHAR[]")},
|
||||
{NodeKind::UTF16, QStringLiteral("WCHAR[]")},
|
||||
};
|
||||
|
||||
auto applyPreset = [&](const QHash<NodeKind, QString>& preset) {
|
||||
for (int row = 0; row < rowMap.size(); row++)
|
||||
table->item(row, 1)->setText(preset.value(kKindMeta[rowMap[row]].kind));
|
||||
};
|
||||
|
||||
connect(btnStdint, &QPushButton::clicked, [&]() { applyPreset(kStdintPreset); });
|
||||
connect(btnWindows, &QPushButton::clicked, [&]() { applyPreset(kWindowsPreset); });
|
||||
|
||||
layout->addWidget(table);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(
|
||||
@@ -2145,10 +2344,10 @@ void MainWindow::showTypeAliasesDialog() {
|
||||
|
||||
// Collect new aliases
|
||||
QHash<NodeKind, QString> newAliases;
|
||||
for (int i = 0; i < rowCount; i++) {
|
||||
QString val = table->item(i, 1)->text().trimmed();
|
||||
for (int row = 0; row < rowMap.size(); row++) {
|
||||
QString val = table->item(row, 1)->text().trimmed();
|
||||
if (!val.isEmpty())
|
||||
newAliases[kKindMeta[i].kind] = val;
|
||||
newAliases[kKindMeta[rowMap[row]].kind] = val;
|
||||
}
|
||||
|
||||
tab->doc->typeAliases = newAliases;
|
||||
@@ -2229,7 +2428,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
int classCount = 0;
|
||||
for (const auto& n : doc->tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
return sub;
|
||||
}
|
||||
@@ -2287,6 +2486,7 @@ void MainWindow::createWorkspaceDock() {
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
|
||||
auto* titleBar = new QWidget(m_workspaceDock);
|
||||
titleBar->setFixedHeight(24);
|
||||
titleBar->setAutoFillBackground(true);
|
||||
{
|
||||
QPalette tbPal = titleBar->palette();
|
||||
@@ -2321,15 +2521,47 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceDock->setTitleBarWidget(titleBar);
|
||||
}
|
||||
|
||||
m_workspaceTree = new QTreeView(m_workspaceDock);
|
||||
// Container widget: search box + tree view
|
||||
auto* dockContainer = new QWidget(m_workspaceDock);
|
||||
auto* dockLayout = new QVBoxLayout(dockContainer);
|
||||
dockLayout->setContentsMargins(0, 0, 0, 0);
|
||||
dockLayout->setSpacing(0);
|
||||
|
||||
m_workspaceSearch = new QLineEdit(dockContainer);
|
||||
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search..."));
|
||||
m_workspaceSearch->setClearButtonEnabled(true);
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: none;"
|
||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
||||
.arg(t.background.name(), t.textDim.name(), t.border.name()));
|
||||
}
|
||||
dockLayout->addWidget(m_workspaceSearch);
|
||||
|
||||
m_workspaceTree = new QTreeView(dockContainer);
|
||||
m_workspaceModel = new QStandardItemModel(this);
|
||||
m_workspaceModel->setHorizontalHeaderLabels({"Name"});
|
||||
m_workspaceTree->setModel(m_workspaceModel);
|
||||
|
||||
m_workspaceProxy = new QSortFilterProxyModel(this);
|
||||
m_workspaceProxy->setSourceModel(m_workspaceModel);
|
||||
m_workspaceProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_workspaceProxy->setRecursiveFilteringEnabled(true);
|
||||
|
||||
m_workspaceTree->setModel(m_workspaceProxy);
|
||||
m_workspaceTree->setHeaderHidden(true);
|
||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
|
||||
connect(m_workspaceSearch, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||
m_workspaceProxy->setFilterFixedString(text);
|
||||
if (!text.isEmpty())
|
||||
m_workspaceTree->expandAll();
|
||||
else
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
});
|
||||
|
||||
// Override palette: selection + hover use theme colors (not default blue)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
@@ -2340,6 +2572,8 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
|
||||
dockLayout->addWidget(m_workspaceTree);
|
||||
|
||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||
@@ -2442,7 +2676,7 @@ void MainWindow::createWorkspaceDock() {
|
||||
}
|
||||
});
|
||||
|
||||
m_workspaceDock->setWidget(m_workspaceTree);
|
||||
m_workspaceDock->setWidget(dockContainer);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||
m_workspaceDock->hide();
|
||||
|
||||
@@ -2463,12 +2697,23 @@ void MainWindow::createWorkspaceDock() {
|
||||
|
||||
m_mdiArea->setActiveSubWindow(sub);
|
||||
|
||||
// Type/Enum node: navigate to it
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
if (ni < 0) return;
|
||||
|
||||
// Child member item: navigate to parent struct, then scroll to this member
|
||||
uint64_t parentId = tree.nodes[ni].parentId;
|
||||
if (parentId != 0) {
|
||||
int pi = tree.indexOfId(parentId);
|
||||
if (pi >= 0) tree.nodes[pi].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(parentId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
} else {
|
||||
// Root type/enum: navigate directly
|
||||
tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2488,7 +2733,7 @@ void MainWindow::rebuildWorkspaceModel() {
|
||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||
}
|
||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
||||
m_workspaceTree->expandToDepth(1);
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
}
|
||||
|
||||
void MainWindow::populateSourceMenu() {
|
||||
@@ -2607,7 +2852,7 @@ void MainWindow::showPluginsDialog() {
|
||||
if (!path.isEmpty()) {
|
||||
if (m_pluginManager.LoadPluginFromPath(path)) {
|
||||
refreshList();
|
||||
m_statusLabel->setText("Plugin loaded successfully");
|
||||
setAppStatus("Plugin loaded successfully");
|
||||
} else {
|
||||
QMessageBox::warning(&dialog, "Failed to Load Plugin",
|
||||
"Could not load the selected plugin.\nCheck the console for details.");
|
||||
@@ -2633,7 +2878,7 @@ void MainWindow::showPluginsDialog() {
|
||||
if (reply == QMessageBox::Yes) {
|
||||
if (m_pluginManager.UnloadPlugin(pluginName)) {
|
||||
refreshList();
|
||||
m_statusLabel->setText("Plugin unloaded");
|
||||
setAppStatus("Plugin unloaded");
|
||||
} else {
|
||||
QMessageBox::warning(&dialog, "Failed to Unload",
|
||||
"Could not unload the selected plugin.");
|
||||
|
||||
@@ -11,14 +11,18 @@
|
||||
#include <QDockWidget>
|
||||
#include <QTreeView>
|
||||
#include <QStandardItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QButtonGroup>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -59,6 +63,11 @@ private slots:
|
||||
void showOptionsDialog();
|
||||
|
||||
public:
|
||||
// Status bar helpers — separate app / MCP channels
|
||||
void setAppStatus(const QString& text);
|
||||
void setMcpStatus(const QString& text);
|
||||
void clearMcpStatus();
|
||||
|
||||
// Project Lifecycle API
|
||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
||||
QMdiSubWindow* project_open(const QString& path = {});
|
||||
@@ -69,7 +78,10 @@ private:
|
||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QLabel* m_statusLabel;
|
||||
ShimmerLabel* m_statusLabel;
|
||||
QString m_appStatus;
|
||||
bool m_mcpBusy = false;
|
||||
QTimer* m_mcpClearTimer = nullptr;
|
||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
||||
QPushButton* m_btnReclass = nullptr;
|
||||
QPushButton* m_btnRendered = nullptr;
|
||||
@@ -127,11 +139,13 @@ private:
|
||||
RcxEditor* activePaneEditor();
|
||||
|
||||
// Workspace dock
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||
QLineEdit* m_workspaceSearch = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel();
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
@@ -170,9 +170,15 @@ void McpBridge::processLine(const QByteArray& line) {
|
||||
}
|
||||
|
||||
if (method == "initialize") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleInitialize(id, req.value("params").toObject()));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/list") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleToolsList(id));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/call") {
|
||||
sendJson(handleToolsCall(id, req.value("params").toObject()));
|
||||
} else {
|
||||
@@ -211,20 +217,29 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
// 1. project.state
|
||||
tools.append(QJsonObject{
|
||||
{"name", "project.state"},
|
||||
{"description", "Returns project state: node tree, base address, sources, provider info. "
|
||||
"Use depth/parentId to avoid dumping the whole tree. "
|
||||
"Call with depth:1 first to see top-level structs, then drill in with parentId."},
|
||||
{"description", "Returns project state with paginated node tree. "
|
||||
"Responses return max 'limit' nodes (default 50). "
|
||||
"Use depth:1 first, then parentId to drill into a struct. "
|
||||
"Enum/bitfield member arrays are omitted by default (counts shown instead); "
|
||||
"pass includeMembers:true to get full arrays. "
|
||||
"Response includes returned/total/nextOffset for paging."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"depth", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max tree depth to return (default 1 = top-level structs only)."}}},
|
||||
{"description", "Max tree depth to return (default 1)."}}},
|
||||
{"parentId", QJsonObject{{"type", "string"},
|
||||
{"description", "Only return children of this node."}}},
|
||||
{"includeTree", QJsonObject{{"type", "boolean"},
|
||||
{"description", "If false, return only provider/source info, no tree. Default true."}}}
|
||||
{"description", "If false, return only provider/source info, no tree. Default true."}}},
|
||||
{"includeMembers", QJsonObject{{"type", "boolean"},
|
||||
{"description", "If true, include full enumMembers/bitfieldMembers arrays. Default false (shows counts only)."}}},
|
||||
{"limit", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max nodes to return (default 50, max 500)."}}},
|
||||
{"offset", QJsonObject{{"type", "integer"},
|
||||
{"description", "Skip this many nodes (for pagination). Use nextOffset from previous response."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
@@ -343,7 +358,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
|
||||
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
||||
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
|
||||
"select_node, refresh"},
|
||||
"select_node, refresh. "
|
||||
"export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -357,6 +373,28 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
// 8. tree.search
|
||||
tools.append(QJsonObject{
|
||||
{"name", "tree.search"},
|
||||
{"description", "Search for nodes by name (substring, case-insensitive). "
|
||||
"Returns compact results: id, name, kind, parentId, offset, childCount. "
|
||||
"Use kindFilter to narrow (e.g. 'Struct'). Max 100 results. "
|
||||
"Much faster than paging through project.state to find a specific type."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||
{"query", QJsonObject{{"type", "string"},
|
||||
{"description", "Name substring to search for (case-insensitive)."}}},
|
||||
{"kindFilter", QJsonObject{{"type", "string"},
|
||||
{"description", "Filter by node kind (e.g. 'Struct', 'Hex64', 'Array')."}}},
|
||||
{"limit", QJsonObject{{"type", "integer"},
|
||||
{"description", "Max results to return (default 20, max 100)."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -368,6 +406,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
QString toolName = params.value("name").toString();
|
||||
QJsonObject args = params.value("arguments").toObject();
|
||||
|
||||
// Show tool activity in status bar (with shimmer)
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
|
||||
QCoreApplication::processEvents(); // paint immediately
|
||||
|
||||
QJsonObject result;
|
||||
if (toolName == "project.state") result = toolProjectState(args);
|
||||
else if (toolName == "tree.apply") result = toolTreeApply(args);
|
||||
@@ -376,8 +418,11 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
else if (toolName == "hex.write") result = toolHexWrite(args);
|
||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
|
||||
return okReply(id, result);
|
||||
}
|
||||
|
||||
@@ -436,6 +481,9 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
|
||||
int maxDepth = args.value("depth").toInt(1);
|
||||
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
|
||||
bool includeMembers = args.value("includeMembers").toBool(false);
|
||||
int limit = qBound(1, args.value("limit").toInt(50), 500);
|
||||
int offset = qMax(0, args.value("offset").toInt(0));
|
||||
QString parentIdStr = args.value("parentId").toString();
|
||||
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
|
||||
|
||||
@@ -481,6 +529,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
state["modified"] = doc->modified;
|
||||
state["undoAvailable"] = doc->undoStack.canUndo();
|
||||
state["redoAvailable"] = doc->undoStack.canRedo();
|
||||
state["statusText"] = m_mainWindow->m_appStatus;
|
||||
|
||||
// Filtered tree: only emit nodes up to maxDepth from the filter root
|
||||
if (includeTree) {
|
||||
@@ -489,12 +538,15 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
// BFS from filterParentId, respecting maxDepth
|
||||
// BFS from filterParentId, respecting maxDepth + pagination
|
||||
QJsonArray nodeArr;
|
||||
struct QueueEntry { uint64_t parentId; int depth; };
|
||||
QVector<QueueEntry> queue;
|
||||
queue.append({filterParentId, 0});
|
||||
|
||||
int totalCount = 0; // total nodes that match depth filter
|
||||
int emitted = 0;
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
auto entry = queue.takeFirst();
|
||||
if (entry.depth > maxDepth) continue;
|
||||
@@ -502,13 +554,47 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
const auto& kids = childMap.value(entry.parentId);
|
||||
for (int ci : kids) {
|
||||
const Node& n = tree.nodes[ci];
|
||||
|
||||
// Count all matching nodes for pagination metadata
|
||||
totalCount++;
|
||||
|
||||
// Apply offset/limit pagination
|
||||
if (totalCount <= offset) {
|
||||
// Still skipping — but enqueue children for counting
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
queue.append({n.id, entry.depth + 1});
|
||||
continue;
|
||||
}
|
||||
if (emitted >= limit) {
|
||||
// Past limit — just keep counting total
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
queue.append({n.id, entry.depth + 1});
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject nj = n.toJson();
|
||||
|
||||
// Strip inline member arrays unless requested
|
||||
if (!includeMembers) {
|
||||
if (nj.contains("enumMembers")) {
|
||||
int count = nj.value("enumMembers").toArray().size();
|
||||
nj.remove("enumMembers");
|
||||
nj["enumMemberCount"] = count;
|
||||
}
|
||||
if (nj.contains("bitfieldMembers")) {
|
||||
int count = nj.value("bitfieldMembers").toArray().size();
|
||||
nj.remove("bitfieldMembers");
|
||||
nj["bitfieldMemberCount"] = count;
|
||||
}
|
||||
}
|
||||
|
||||
// Add computed size for containers
|
||||
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
|
||||
nj["computedSize"] = tree.structSpan(n.id, &childMap);
|
||||
nj["childCount"] = childMap.value(n.id).size();
|
||||
}
|
||||
nodeArr.append(nj);
|
||||
emitted++;
|
||||
|
||||
// Enqueue children if we haven't hit depth limit
|
||||
if (entry.depth + 1 <= maxDepth)
|
||||
@@ -520,6 +606,10 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
||||
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
|
||||
treeObj["nextId"] = QString::number(tree.m_nextId);
|
||||
treeObj["nodes"] = nodeArr;
|
||||
treeObj["returned"] = emitted;
|
||||
treeObj["total"] = totalCount;
|
||||
if (emitted < totalCount)
|
||||
treeObj["nextOffset"] = offset + emitted;
|
||||
state["tree"] = treeObj;
|
||||
}
|
||||
|
||||
@@ -956,7 +1046,7 @@ QJsonObject McpBridge::toolStatusSet(const QJsonObject& args) {
|
||||
}
|
||||
}
|
||||
if (target == "statusBar" || target == "both") {
|
||||
m_mainWindow->m_statusLabel->setText(text);
|
||||
m_mainWindow->setAppStatus(text);
|
||||
}
|
||||
|
||||
return makeTextResult("Status set: " + text);
|
||||
@@ -1004,7 +1094,24 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
||||
if (action == "export_cpp") {
|
||||
if (!doc) return makeTextResult("No active tab", true);
|
||||
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
|
||||
QString code = renderCppAll(doc->tree, aliases);
|
||||
QString code;
|
||||
if (!nodeIdStr.isEmpty()) {
|
||||
// Per-struct export
|
||||
uint64_t nid = nodeIdStr.toULongLong();
|
||||
code = renderCpp(doc->tree, nid, aliases);
|
||||
if (code.isEmpty())
|
||||
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
|
||||
} else {
|
||||
code = renderCppAll(doc->tree, aliases);
|
||||
}
|
||||
// Truncate if too large (64 KB limit)
|
||||
if (code.size() > 65536) {
|
||||
int totalSize = code.size();
|
||||
code.truncate(65536);
|
||||
code += QStringLiteral("\n\n... truncated (%1 bytes total, showing first 64KB)"
|
||||
"\nUse nodeId param to export a single struct.")
|
||||
.arg(totalSize);
|
||||
}
|
||||
return makeTextResult(code);
|
||||
}
|
||||
if (action == "save_file") {
|
||||
@@ -1053,6 +1160,70 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
||||
return makeTextResult("Unknown action: " + action, true);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: tree.search
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
const auto& tree = tab->doc->tree;
|
||||
QString query = args.value("query").toString();
|
||||
QString kindFilter = args.value("kindFilter").toString();
|
||||
int limit = qBound(1, args.value("limit").toInt(20), 100);
|
||||
|
||||
if (query.isEmpty() && kindFilter.isEmpty())
|
||||
return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true);
|
||||
|
||||
// Build parent→children map for childCount
|
||||
QHash<uint64_t, int> childCounts;
|
||||
for (const auto& n : tree.nodes)
|
||||
childCounts[n.parentId]++;
|
||||
|
||||
QJsonArray results;
|
||||
for (const auto& n : tree.nodes) {
|
||||
// Kind filter
|
||||
if (!kindFilter.isEmpty()) {
|
||||
if (kindToString(n.kind) != kindFilter) continue;
|
||||
}
|
||||
// Name substring match (case-insensitive)
|
||||
if (!query.isEmpty()) {
|
||||
bool nameMatch = n.name.contains(query, Qt::CaseInsensitive);
|
||||
bool typeMatch = n.structTypeName.contains(query, Qt::CaseInsensitive);
|
||||
if (!nameMatch && !typeMatch) continue;
|
||||
}
|
||||
|
||||
QJsonObject nj;
|
||||
nj["id"] = QString::number(n.id);
|
||||
nj["name"] = n.name;
|
||||
nj["kind"] = kindToString(n.kind);
|
||||
nj["parentId"] = QString::number(n.parentId);
|
||||
nj["offset"] = n.offset;
|
||||
if (!n.structTypeName.isEmpty())
|
||||
nj["structTypeName"] = n.structTypeName;
|
||||
if (!n.classKeyword.isEmpty())
|
||||
nj["classKeyword"] = n.classKeyword;
|
||||
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array)
|
||||
nj["childCount"] = childCounts.value(n.id, 0);
|
||||
if (!n.enumMembers.isEmpty())
|
||||
nj["enumMemberCount"] = n.enumMembers.size();
|
||||
if (!n.bitfieldMembers.isEmpty())
|
||||
nj["bitfieldMemberCount"] = n.bitfieldMembers.size();
|
||||
results.append(nj);
|
||||
|
||||
if (results.size() >= limit) break;
|
||||
}
|
||||
|
||||
QJsonObject out;
|
||||
out["results"] = results;
|
||||
out["count"] = results.size();
|
||||
out["query"] = query;
|
||||
if (!kindFilter.isEmpty()) out["kindFilter"] = kindFilter;
|
||||
return makeTextResult(QString::fromUtf8(
|
||||
QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -58,6 +58,7 @@ private:
|
||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -16,7 +16,7 @@ struct OptionsResult {
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = false;
|
||||
bool autoStartMcp = true;
|
||||
int refreshMs = 660;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#585858",
|
||||
"textFaint": "#505050",
|
||||
"hover": "#1e1e1e",
|
||||
"selected": "#1e1e1e",
|
||||
"hover": "#2a2a2a",
|
||||
"selected": "#2a2d2e",
|
||||
"selection": "#2b2b2b",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
|
||||
@@ -29,46 +29,88 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
QVector<std::pair<const Node*, void*>> types, enums;
|
||||
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
|
||||
QVector<Entry> types, enums;
|
||||
for (const auto& tab : tabs) {
|
||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||
for (int idx : topLevel) {
|
||||
const Node& n = tab.tree->nodes[idx];
|
||||
if (n.kind != NodeKind::Struct) continue;
|
||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
enums.append({&n, tab.subPtr});
|
||||
enums.append({&n, tab.subPtr, tab.tree});
|
||||
else
|
||||
types.append({&n, tab.subPtr});
|
||||
types.append({&n, tab.subPtr, tab.tree});
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
||||
const std::pair<const Node*, void*>& b) {
|
||||
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
||||
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
for (const auto& [n, subPtr] : types) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
// Helper: type display string for a member node
|
||||
auto memberTypeName = [](const Node& m) -> QString {
|
||||
if (m.kind == NodeKind::Struct) {
|
||||
QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||
: m.structTypeName;
|
||||
return stn;
|
||||
}
|
||||
return QString::fromLatin1(kindToString(m.kind));
|
||||
};
|
||||
|
||||
// Helper: is a Hex padding node
|
||||
auto isHexPad = [](NodeKind k) {
|
||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||
};
|
||||
|
||||
for (const auto& e : types) {
|
||||
QVector<int> members = e.tree->childrenOf(e.node->id);
|
||||
|
||||
// Count non-hex members for display
|
||||
int visibleCount = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount;
|
||||
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(visibleCount));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
|
||||
// Add child rows sorted by offset (skip Hex padding)
|
||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||
return e.tree->nodes[a].offset < e.tree->nodes[b].offset;
|
||||
});
|
||||
for (int mi : members) {
|
||||
const Node& m = e.tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
QString childDisplay = QStringLiteral("%1 %2")
|
||||
.arg(memberTypeName(m), m.name);
|
||||
auto* childItem = new QStandardItem(childDisplay);
|
||||
childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||
item->appendRow(childItem);
|
||||
}
|
||||
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
for (const auto& [n, subPtr] : enums) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
for (const auto& e : enums) {
|
||||
int count = e.node->enumMembers.size();
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(count));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) {
|
||||
QString src = buildSourceLabel(prov);
|
||||
QString addr = QStringLiteral("0x") +
|
||||
QString::number(baseAddress, 16).toUpper();
|
||||
return QStringLiteral(" %1 \u00B7 %2").arg(src, addr);
|
||||
return QStringLiteral(" %1 %2").arg(src, addr);
|
||||
}
|
||||
|
||||
// -- Replicate commandRowSrcSpan for testing
|
||||
@@ -32,17 +32,13 @@ struct TestColumnSpan {
|
||||
};
|
||||
|
||||
static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
class TestCommandRow : public QObject {
|
||||
@@ -77,13 +73,13 @@ private slots:
|
||||
void row_nullProvider() {
|
||||
NullProvider p;
|
||||
QString row = buildCommandRow(p, 0);
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE \u00B7 0x0"));
|
||||
QCOMPARE(row, QStringLiteral(" source\u25BE 0x0"));
|
||||
}
|
||||
|
||||
void row_fileProvider() {
|
||||
BufferProvider p(QByteArray(4, '\0'), "test.bin");
|
||||
QString row = buildCommandRow(p, 0x140000000ULL);
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE \u00B7 0x140000000"));
|
||||
QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE 0x140000000"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
@@ -110,7 +106,7 @@ private slots:
|
||||
void span_processProvider_simulated() {
|
||||
// Simulate a process provider without needing Windows APIs
|
||||
// by building the string directly
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE \u00B7 0x7FF600000000");
|
||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE 0x7FF600000000");
|
||||
auto span = commandRowSrcSpan(row);
|
||||
QVERIFY(span.valid);
|
||||
QString extracted = row.mid(span.start, span.end - span.start);
|
||||
|
||||
@@ -1924,7 +1924,7 @@ private slots:
|
||||
|
||||
void testCommandRowRootNameSpan() {
|
||||
// Name span should cover the class name in the merged command row
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
|
||||
QString text = "source\u25BE 0x0 struct MyClass {";
|
||||
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
@@ -2173,8 +2173,8 @@ private slots:
|
||||
QVERIFY(result.text.contains("Blue"));
|
||||
QVERIFY(result.text.contains("= 0"));
|
||||
QVERIFY(result.text.contains("= 2"));
|
||||
// Header should contain "enum"
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
// Header should contain the type name
|
||||
QVERIFY(result.text.contains("Color"));
|
||||
}
|
||||
|
||||
void testEnumCollapsed() {
|
||||
@@ -2205,8 +2205,7 @@ private slots:
|
||||
// Collapsed: members should NOT appear
|
||||
QVERIFY(!result.text.contains("= 0"));
|
||||
QVERIFY(!result.text.contains("= 1"));
|
||||
// But header should still show
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
// But header should still show the type name
|
||||
QVERIFY(result.text.contains("Flags"));
|
||||
}
|
||||
|
||||
@@ -2351,6 +2350,91 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
void testBitfieldMembers() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("Test");
|
||||
root.structTypeName = QStringLiteral("Test");
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node bf;
|
||||
bf.kind = NodeKind::Struct;
|
||||
bf.classKeyword = QStringLiteral("bitfield");
|
||||
bf.name = QStringLiteral("flags");
|
||||
bf.elementKind = NodeKind::Hex32;
|
||||
bf.parentId = rootId;
|
||||
bf.offset = 0;
|
||||
bf.collapsed = false;
|
||||
bf.bitfieldMembers = {
|
||||
{QStringLiteral("Valid"), 0, 1},
|
||||
{QStringLiteral("Dirty"), 1, 1},
|
||||
{QStringLiteral("PageNum"), 2, 20}
|
||||
};
|
||||
tree.addNode(bf);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Should contain bitfield member names
|
||||
QVERIFY(result.text.contains(QStringLiteral("Valid")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("Dirty")));
|
||||
QVERIFY(result.text.contains(QStringLiteral("PageNum")));
|
||||
// Should contain : width = value format
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 1 =")));
|
||||
QVERIFY(result.text.contains(QStringLiteral(": 20 =")));
|
||||
// Member lines should have isMemberLine set
|
||||
bool foundMemberLine = false;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.isMemberLine) {
|
||||
foundMemberLine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundMemberLine);
|
||||
}
|
||||
|
||||
void testBitfieldJsonRoundtrip() {
|
||||
Node n;
|
||||
n.id = 42;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
n.bitfieldMembers = {
|
||||
{QStringLiteral("ExecuteDisable"), 63, 1},
|
||||
{QStringLiteral("PageFrameNumber"), 12, 36}
|
||||
};
|
||||
|
||||
QJsonObject json = n.toJson();
|
||||
Node restored = Node::fromJson(json);
|
||||
|
||||
QCOMPARE(restored.classKeyword, QStringLiteral("bitfield"));
|
||||
QCOMPARE(restored.bitfieldMembers.size(), 2);
|
||||
QCOMPARE(restored.bitfieldMembers[0].name, QStringLiteral("ExecuteDisable"));
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitOffset, (uint8_t)63);
|
||||
QCOMPARE(restored.bitfieldMembers[0].bitWidth, (uint8_t)1);
|
||||
QCOMPARE(restored.bitfieldMembers[1].name, QStringLiteral("PageFrameNumber"));
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitOffset, (uint8_t)12);
|
||||
QCOMPARE(restored.bitfieldMembers[1].bitWidth, (uint8_t)36);
|
||||
}
|
||||
|
||||
void testBitfieldByteSize() {
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.classKeyword = QStringLiteral("bitfield");
|
||||
n.elementKind = NodeKind::Hex8;
|
||||
QCOMPARE(n.byteSize(), 1);
|
||||
n.elementKind = NodeKind::Hex16;
|
||||
QCOMPARE(n.byteSize(), 2);
|
||||
n.elementKind = NodeKind::Hex32;
|
||||
QCOMPARE(n.byteSize(), 4);
|
||||
n.elementKind = NodeKind::Hex64;
|
||||
QCOMPARE(n.byteSize(), 8);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -481,7 +481,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// BaseAddress should be ALLOWED on CommandRow (ADDR field)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||
@@ -816,7 +816,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with ADDR value (simulates controller)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// Line 0 is CommandRow
|
||||
const LineMeta* lm = m_editor->metaForLine(0);
|
||||
@@ -901,7 +901,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with ADDR value (simulates controller)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||
|
||||
// Begin base address edit on line 0 (CommandRow ADDR field)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||
@@ -1038,7 +1038,7 @@ private slots:
|
||||
|
||||
// Set CommandRow text with root class (simulates controller.updateCommandRow)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {"));
|
||||
|
||||
// RootClassName should be allowed on CommandRow (line 0)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
|
||||
@@ -1053,7 +1053,7 @@ private slots:
|
||||
|
||||
// Set CommandRow with root class
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {"));
|
||||
|
||||
// Line 0 is CommandRow
|
||||
const LineMeta* lm = m_editor->metaForLine(0);
|
||||
@@ -1099,7 +1099,7 @@ private slots:
|
||||
|
||||
// Set command row text (simulates controller.updateCommandRow)
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
@@ -1177,7 +1177,7 @@ private slots:
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ private slots:
|
||||
}
|
||||
|
||||
void testFmtPointer64_null() {
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL"));
|
||||
QCOMPARE(fmt::fmtPointer64(0), QString("0x0"));
|
||||
}
|
||||
|
||||
void testFmtPointer64_nonNull() {
|
||||
QString s = fmt::fmtPointer64(0x400000);
|
||||
QVERIFY(s.startsWith("-> 0x"));
|
||||
QVERIFY(s.startsWith("0x"));
|
||||
QVERIFY(s.contains("400000"));
|
||||
}
|
||||
|
||||
|
||||
@@ -780,7 +780,7 @@ void TestImportSource::structPrefixOnType() {
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldSkipped() {
|
||||
// Bitfields emit a hex placeholder covering the group
|
||||
// Bitfields emit a bitfield container with named members
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF {\n"
|
||||
" uint32_t normal;\n"
|
||||
@@ -790,12 +790,20 @@ void TestImportSource::bitfieldSkipped() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + Hex16 (16 bits → 2 bytes) + after
|
||||
// normal + bitfield container (16 bits → 2 bytes) + after
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("bitA"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitOffset, (uint8_t)0);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("bitB"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitWidth, (uint8_t)12);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitOffset, (uint8_t)4);
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 6);
|
||||
}
|
||||
@@ -812,13 +820,22 @@ void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + hex64 (bitfield group: 64 bits) + after = 3
|
||||
// normal + bitfield container (64 bits) + after = 3
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
// Bitfield group emitted as Hex64 at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||
// Bitfield container at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].elementKind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 4);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("Valid"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)1);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("Dirty"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].name, QStringLiteral("PageFrameNumber"));
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].bitWidth, (uint8_t)36);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[3].name, QStringLiteral("Reserved"));
|
||||
// after at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
|
||||
@@ -63,7 +63,7 @@ private slots:
|
||||
// ── Chevron span detection ──
|
||||
|
||||
void testChevronSpanDetected() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {");
|
||||
ColumnSpan span = commandRowChevronSpan(text);
|
||||
QVERIFY(span.valid);
|
||||
QCOMPARE(span.start, 0);
|
||||
@@ -80,7 +80,7 @@ private slots:
|
||||
// ── Existing spans unbroken by chevron prefix ──
|
||||
|
||||
void testSpansWithPrefix() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {");
|
||||
|
||||
ColumnSpan src = commandRowSrcSpan(text);
|
||||
QVERIFY(src.valid);
|
||||
|
||||
Reference in New Issue
Block a user