mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
13 Commits
snapshot-2
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52f751e751 | ||
|
|
0a19789a9d | ||
|
|
62a68bef80 | ||
|
|
4941f860b6 | ||
|
|
c45d51d736 | ||
|
|
5b46065403 | ||
|
|
4706f7b782 | ||
|
|
fe9bfafa3b | ||
|
|
ff928df685 | ||
|
|
d6e3c182fc | ||
|
|
d7a6e1862e | ||
|
|
1ddf47a754 | ||
|
|
1a885a8b1d |
46
README.md
46
README.md
@@ -1,8 +1,11 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# Reclass
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="docs/RECLASS_LIGHTMODE.svg" height="170">
|
||||||
|
<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)
|
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
|
||||||
|
|
||||||
@@ -13,30 +16,41 @@
|
|||||||
|
|
||||||
</div>
|
</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.
|
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
|
## Features
|
||||||
|
|
||||||
- **Structured binary view** — render raw bytes as typed fields (integers, floats, pointers, vectors, matrices, strings, booleans, padding)
|
- **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
|
- **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
|
- **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
|
- **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
|
- **Split views** — multiple synchronized editor panes over the same document
|
||||||
- **Type autocomplete** — popup type picker when changing field kinds
|
- **Type autocomplete** — popup type picker when changing field kinds
|
||||||
- **Hex + ASCII margins** — raw byte previews alongside the structured view
|
- **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
|
- **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
|
- **Process plugin** — access memory of live processes on Windows and Linux
|
||||||
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
|
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
|
||||||
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
|
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
|
||||||
|
|
||||||
---
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Process memory section enumeration
|
||||||
|
- [ ] Address parser auto-complete
|
||||||
|
- [ ] Safe mode
|
||||||
|
- [ ] File import for other Reclass instances
|
||||||
|
- [ ] Expose UI functionality to plugins
|
||||||
|
- [ ] iOS/macOS support
|
||||||
|
- [ ] Display RTTI information
|
||||||
|
|
||||||
## Data Sources
|
## Data Sources
|
||||||
|
|
||||||
@@ -45,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
|
- **Remote Process** — read another process's memory via shared memory
|
||||||
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||

|

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

|

|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## MCP Integration
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
@@ -72,13 +82,11 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
### Prerequisites
|
### 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)
|
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
|
||||||
- **Ninja** — bundled with the Qt installer
|
- **Ninja** — bundled with the Qt installer
|
||||||
|
|
||||||
@@ -110,15 +118,11 @@ The build script auto-detects your Qt install location.
|
|||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build --output-on-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Alternatives
|
## Alternatives
|
||||||
|
|
||||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||||
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<sub>MIT License</sub>
|
<sub>MIT License</sub>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
160
docs/RECLASS_DARKMODE.svg
Normal file
160
docs/RECLASS_DARKMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_130-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_236-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_225-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #1f2939;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #5d9bd4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #1e3e88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #6e809a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_225);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-9 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_236);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_130);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-11 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_170);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-12 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_161);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-13 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_183);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-14 {
|
||||||
|
fill: #b06ba9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-15 {
|
||||||
|
fill: #826415;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-16 {
|
||||||
|
fill: #e2aa11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-17 {
|
||||||
|
fill: #893089;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#f3db78"/>
|
||||||
|
<stop offset=".19" stop-color="#f4e188"/>
|
||||||
|
<stop offset=".34" stop-color="#f4e38d"/>
|
||||||
|
<stop offset=".38" stop-color="#f4df81"/>
|
||||||
|
<stop offset=".47" stop-color="#f5d86f"/>
|
||||||
|
<stop offset=".57" stop-color="#f5d463"/>
|
||||||
|
<stop offset=".67" stop-color="#f6d360"/>
|
||||||
|
<stop offset=".89" stop-color="#f1cc53"/>
|
||||||
|
<stop offset="1" stop-color="#efbe33"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".18" stop-color="#e2aa11"/>
|
||||||
|
<stop offset=".91" stop-color="#826415"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#deb0d3"/>
|
||||||
|
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||||
|
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||||
|
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||||
|
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||||
|
<stop offset=".67" stop-color="#cd99c5"/>
|
||||||
|
<stop offset=".89" stop-color="#c68abc"/>
|
||||||
|
<stop offset="1" stop-color="#bb7db4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".19" stop-color="#b06ba9"/>
|
||||||
|
<stop offset=".87" stop-color="#893089"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#91c4eb"/>
|
||||||
|
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||||
|
<stop offset=".33" stop-color="#96c6ec"/>
|
||||||
|
<stop offset=".35" stop-color="#91c3ea"/>
|
||||||
|
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||||
|
<stop offset=".56" stop-color="#73b2e2"/>
|
||||||
|
<stop offset=".67" stop-color="#70b0e1"/>
|
||||||
|
<stop offset=".89" stop-color="#60a7dc"/>
|
||||||
|
<stop offset="1" stop-color="#4d9bd5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||||
|
<stop offset=".87" stop-color="#1e3e88"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
|
||||||
|
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
|
||||||
|
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
|
||||||
|
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
|
||||||
|
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
|
||||||
|
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
|
||||||
|
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
|
||||||
|
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
|
||||||
|
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
|
||||||
|
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
|
||||||
|
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
|
||||||
|
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
|
||||||
|
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
|
||||||
|
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
|
||||||
|
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
|
||||||
|
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
|
||||||
|
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
|
||||||
|
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||||
|
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.4 KiB |
160
docs/RECLASS_LIGHTMODE.svg
Normal file
160
docs/RECLASS_LIGHTMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_130-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_236-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_225-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #5d9bd4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #e3e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #1e3e88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #6e809a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_225);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-9 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_236);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-10 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_130);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-11 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_170);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-12 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_161);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-13 {
|
||||||
|
fill: url(#Unbenannter_Verlauf_183);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-14 {
|
||||||
|
fill: #b06ba9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-15 {
|
||||||
|
fill: #826415;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-16 {
|
||||||
|
fill: #e2aa11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-17 {
|
||||||
|
fill: #893089;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#f3db78"/>
|
||||||
|
<stop offset=".19" stop-color="#f4e188"/>
|
||||||
|
<stop offset=".34" stop-color="#f4e38d"/>
|
||||||
|
<stop offset=".38" stop-color="#f4df81"/>
|
||||||
|
<stop offset=".47" stop-color="#f5d86f"/>
|
||||||
|
<stop offset=".57" stop-color="#f5d463"/>
|
||||||
|
<stop offset=".67" stop-color="#f6d360"/>
|
||||||
|
<stop offset=".89" stop-color="#f1cc53"/>
|
||||||
|
<stop offset="1" stop-color="#efbe33"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".18" stop-color="#e2aa11"/>
|
||||||
|
<stop offset=".91" stop-color="#826415"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#deb0d3"/>
|
||||||
|
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||||
|
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||||
|
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||||
|
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||||
|
<stop offset=".67" stop-color="#cd99c5"/>
|
||||||
|
<stop offset=".89" stop-color="#c68abc"/>
|
||||||
|
<stop offset="1" stop-color="#bb7db4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".19" stop-color="#b06ba9"/>
|
||||||
|
<stop offset=".87" stop-color="#893089"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#91c4eb"/>
|
||||||
|
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||||
|
<stop offset=".33" stop-color="#96c6ec"/>
|
||||||
|
<stop offset=".35" stop-color="#91c3ea"/>
|
||||||
|
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||||
|
<stop offset=".56" stop-color="#73b2e2"/>
|
||||||
|
<stop offset=".67" stop-color="#70b0e1"/>
|
||||||
|
<stop offset=".89" stop-color="#60a7dc"/>
|
||||||
|
<stop offset="1" stop-color="#4d9bd5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||||
|
<stop offset=".87" stop-color="#1e3e88"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
|
||||||
|
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
|
||||||
|
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
|
||||||
|
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
|
||||||
|
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
|
||||||
|
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
|
||||||
|
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
|
||||||
|
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
|
||||||
|
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
|
||||||
|
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
|
||||||
|
<g>
|
||||||
|
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
|
||||||
|
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
|
||||||
|
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
|
||||||
|
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
|
||||||
|
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
|
||||||
|
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
|
||||||
|
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
|
||||||
|
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
|
||||||
|
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||||
|
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.4 KiB |
194
src/compose.cpp
194
src/compose.cpp
@@ -22,6 +22,7 @@ struct ComposeState {
|
|||||||
int nameW = kColName; // global name column width (fallback)
|
int nameW = kColName; // global name column width (fallback)
|
||||||
int offsetHexDigits = 8; // hex digit tier for offset margin
|
int offsetHexDigits = 8; // hex digit tier for offset margin
|
||||||
bool baseEmitted = false; // only first root struct shows base address
|
bool baseEmitted = false; // only first root struct shows base address
|
||||||
|
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||||
|
|
||||||
// Precomputed for O(1) lookups
|
// Precomputed for O(1) lookups
|
||||||
@@ -104,6 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
|
|||||||
return state.absOffsets[nodeIdx];
|
return state.absOffsets[nodeIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
|
||||||
|
static const QVector<int> kEmpty;
|
||||||
|
auto it = state.childMap.constFind(parentId);
|
||||||
|
return it == state.childMap.constEnd() ? kEmpty : it.value();
|
||||||
|
}
|
||||||
|
|
||||||
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||||
const Provider& prov, int nodeIdx,
|
const Provider& prov, int nodeIdx,
|
||||||
int depth, uint64_t absAddr, uint64_t scopeId) {
|
int depth, uint64_t absAddr, uint64_t scopeId) {
|
||||||
@@ -132,6 +140,11 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect type overflow in compact mode (for effectiveTypeW)
|
||||||
|
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
|
||||||
|
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
|
||||||
|
int lineTypeW = typeOverflow ? rawType.size() : typeW;
|
||||||
|
|
||||||
for (int sub = 0; sub < numLines; sub++) {
|
for (int sub = 0; sub < numLines; sub++) {
|
||||||
bool isCont = (sub > 0);
|
bool isCont = (sub > 0);
|
||||||
|
|
||||||
@@ -148,7 +161,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||||
lm.foldLevel = computeFoldLevel(depth, false);
|
lm.foldLevel = computeFoldLevel(depth, false);
|
||||||
lm.effectiveTypeW = typeW;
|
lm.effectiveTypeW = lineTypeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
lm.pointerTargetName = ptrTargetName;
|
lm.pointerTargetName = ptrTargetName;
|
||||||
|
|
||||||
@@ -158,7 +171,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||||
|
state.compactColumns);
|
||||||
state.emitLine(lineText, lm);
|
state.emitLine(lineText, lm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,8 +262,6 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldCollapsed = node.collapsed;
|
lm.foldCollapsed = node.collapsed;
|
||||||
lm.foldLevel = computeFoldLevel(depth, true);
|
lm.foldLevel = computeFoldLevel(depth, true);
|
||||||
lm.markerMask = (1u << M_STRUCT_BG);
|
lm.markerMask = (1u << M_STRUCT_BG);
|
||||||
lm.effectiveTypeW = typeW;
|
|
||||||
lm.effectiveNameW = nameW;
|
|
||||||
|
|
||||||
QString headerText;
|
QString headerText;
|
||||||
if (node.kind == NodeKind::Array) {
|
if (node.kind == NodeKind::Array) {
|
||||||
@@ -260,19 +272,129 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.arrayCount = node.arrayLen;
|
lm.arrayCount = node.arrayLen;
|
||||||
QString elemStructName = (node.elementKind == NodeKind::Struct)
|
QString elemStructName = (node.elementKind == NodeKind::Struct)
|
||||||
? resolvePointerTarget(tree, node.refId) : QString();
|
? resolvePointerTarget(tree, node.refId) : QString();
|
||||||
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName);
|
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||||
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||||
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||||
|
lm.effectiveNameW = nameW;
|
||||||
|
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
|
||||||
} else {
|
} else {
|
||||||
// All structs (root and nested) use the same header format
|
// All structs (root and nested) use the same header format
|
||||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
|
QString rawType = fmt::structTypeName(node);
|
||||||
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||||
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||||
|
lm.effectiveNameW = nameW;
|
||||||
|
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
||||||
}
|
}
|
||||||
state.emitLine(headerText, lm);
|
state.emitLine(headerText, lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||||
QVector<int> children = state.childMap.value(node.id);
|
// Enum with members: render name = value lines instead of offset-based fields
|
||||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
int childDepth = depth + 1;
|
||||||
});
|
int maxNameLen = 4;
|
||||||
|
for (const auto& m : node.enumMembers)
|
||||||
|
maxNameLen = qMax(maxNameLen, (int)m.first.size());
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
lm.nodeId = node.id;
|
||||||
|
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;
|
||||||
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||||
|
lm.offsetAddr = absAddr;
|
||||||
|
lm.ptrBase = state.currentPtrBase;
|
||||||
|
state.emitLine(fmt::fmtEnumMember(m.first, m.second, 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;
|
||||||
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||||
|
lm.offsetAddr = absAddr;
|
||||||
|
lm.ptrBase = state.currentPtrBase;
|
||||||
|
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.visiting.remove(node.id);
|
||||||
|
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;
|
int childDepth = depth + 1;
|
||||||
|
|
||||||
@@ -309,11 +431,13 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
||||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||||
lm.effectiveTypeW = eTW;
|
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
|
||||||
|
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
|
||||||
lm.effectiveNameW = eNW;
|
lm.effectiveNameW = eNW;
|
||||||
|
|
||||||
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
||||||
{}, eTW, eNW, elemTypeStr), lm);
|
{}, eTW, eNW, elemTypeStr,
|
||||||
|
state.compactColumns), lm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,10 +463,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
int refIdx = tree.indexOfId(node.refId);
|
||||||
if (refIdx >= 0) {
|
if (refIdx >= 0) {
|
||||||
QVector<int> refChildren = state.childMap.value(node.refId);
|
const QVector<int>& refChildren = childIndices(state, node.refId);
|
||||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
|
||||||
});
|
|
||||||
// Use the referenced struct's scope widths (children come from there)
|
// Use the referenced struct's scope widths (children come from there)
|
||||||
uint64_t refScopeId = node.refId;
|
uint64_t refScopeId = node.refId;
|
||||||
for (int childIdx : refChildren) {
|
for (int childIdx : refChildren) {
|
||||||
@@ -351,6 +472,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
if (state.visiting.contains(child.id)) {
|
if (state.visiting.contains(child.id)) {
|
||||||
int typeW = state.effectiveTypeW(refScopeId);
|
int typeW = state.effectiveTypeW(refScopeId);
|
||||||
int nameW = state.effectiveNameW(refScopeId);
|
int nameW = state.effectiveNameW(refScopeId);
|
||||||
|
QString rawType = fmt::structTypeName(child);
|
||||||
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||||
LineMeta lm;
|
LineMeta lm;
|
||||||
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
||||||
lm.nodeId = child.id;
|
lm.nodeId = child.id;
|
||||||
@@ -366,10 +489,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldCollapsed = true;
|
lm.foldCollapsed = true;
|
||||||
lm.foldLevel = computeFoldLevel(childDepth, true);
|
lm.foldLevel = computeFoldLevel(childDepth, true);
|
||||||
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
||||||
lm.effectiveTypeW = typeW;
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||||
/*collapsed=*/true, typeW, nameW), lm);
|
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
composeNode(state, tree, prov, childIdx, childDepth,
|
composeNode(state, tree, prov, childIdx, childDepth,
|
||||||
@@ -431,7 +554,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||||
|
|
||||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||||
QVector<int> ptrChildren = state.childMap.value(node.id);
|
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
||||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||||
|
|
||||||
// Force collapsed if this refId is already being virtually expanded
|
// Force collapsed if this refId is already being virtually expanded
|
||||||
@@ -458,12 +581,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.foldLevel = computeFoldLevel(depth, true);
|
lm.foldLevel = computeFoldLevel(depth, true);
|
||||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||||
lm.effectiveTypeW = typeW;
|
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
|
||||||
|
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||||
lm.effectiveNameW = nameW;
|
lm.effectiveNameW = nameW;
|
||||||
lm.pointerTargetName = ptrTargetName;
|
lm.pointerTargetName = ptrTargetName;
|
||||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||||
prov, absAddr, ptrTypeOverride,
|
prov, absAddr, ptrTypeOverride,
|
||||||
typeW, nameW), lm);
|
typeW, nameW, state.compactColumns), lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!effectiveCollapsed) {
|
if (!effectiveCollapsed) {
|
||||||
@@ -496,9 +620,6 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
// Render materialized children at the pointer target address.
|
// Render materialized children at the pointer target address.
|
||||||
// These are real tree nodes with independent state — use rootId
|
// These are real tree nodes with independent state — use rootId
|
||||||
// so resolveAddr computes offsets relative to the pointer target.
|
// so resolveAddr computes offsets relative to the pointer target.
|
||||||
std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
|
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
|
||||||
});
|
|
||||||
for (int childIdx : ptrChildren) {
|
for (int childIdx : ptrChildren) {
|
||||||
composeNode(state, tree, childProv, childIdx, depth + 1,
|
composeNode(state, tree, childProv, childIdx, depth + 1,
|
||||||
pBase, node.id, false, node.id);
|
pBase, node.id, false, node.id);
|
||||||
@@ -558,13 +679,22 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
|||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||||
|
bool compactColumns) {
|
||||||
ComposeState state;
|
ComposeState state;
|
||||||
|
state.compactColumns = compactColumns;
|
||||||
|
|
||||||
// Precompute parent→children map
|
// Precompute parent→children map
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
state.childMap[tree.nodes[i].parentId].append(i);
|
state.childMap[tree.nodes[i].parentId].append(i);
|
||||||
|
|
||||||
|
for (auto it = state.childMap.begin(); it != state.childMap.end(); ++it) {
|
||||||
|
QVector<int>& children = it.value();
|
||||||
|
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||||
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||||
state.absOffsets.resize(tree.nodes.size());
|
state.absOffsets.resize(tree.nodes.size());
|
||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
@@ -599,11 +729,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
|
|
||||||
// Compute effective type column width from longest type name
|
// Compute effective type column width from longest type name
|
||||||
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
||||||
|
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
|
||||||
int maxTypeLen = kMinTypeW;
|
int maxTypeLen = kMinTypeW;
|
||||||
for (const Node& node : tree.nodes) {
|
for (const Node& node : tree.nodes) {
|
||||||
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
||||||
}
|
}
|
||||||
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
|
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
|
||||||
|
|
||||||
// Compute effective name column width from longest name
|
// Compute effective name column width from longest name
|
||||||
// Include struct/array names - they now use columnar layout too
|
// Include struct/array names - they now use columnar layout too
|
||||||
@@ -647,7 +778,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
|
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
|
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
|
||||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,12 +796,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
|
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
|
||||||
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
|
// 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;
|
LineMeta lm;
|
||||||
lm.nodeIdx = -1;
|
lm.nodeIdx = -1;
|
||||||
@@ -688,10 +819,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
|||||||
state.emitLine(cmdRowText, lm);
|
state.emitLine(cmdRowText, lm);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<int> roots = state.childMap.value(0);
|
const QVector<int>& roots = childIndices(state, 0);
|
||||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
|
||||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (int idx : roots) {
|
for (int idx : roots) {
|
||||||
// If viewRootId is set, skip roots that don't match
|
// If viewRootId is set, skip roots that don't match
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult RcxDocument::compose(uint64_t viewRootId) const {
|
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const {
|
||||||
return rcx::compose(tree, *provider, viewRootId);
|
return rcx::compose(tree, *provider, viewRootId, compactColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RcxDocument::save(const QString& path) {
|
bool RcxDocument::save(const QString& path) {
|
||||||
@@ -250,6 +250,15 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
if (text.isEmpty()) break;
|
if (text.isEmpty()) break;
|
||||||
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
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
|
// ASCII edit on Hex nodes
|
||||||
if (isHexPreview(node.kind)) {
|
if (isHexPreview(node.kind)) {
|
||||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
|
||||||
@@ -321,9 +330,27 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
|||||||
}
|
}
|
||||||
break;
|
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);
|
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case EditTarget::BaseAddress: {
|
case EditTarget::BaseAddress: {
|
||||||
QString s = text.trimmed();
|
QString s = text.trimmed();
|
||||||
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
|
||||||
@@ -493,9 +520,9 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
// Compose against snapshot provider if active, otherwise real provider
|
// Compose against snapshot provider if active, otherwise real provider
|
||||||
if (m_snapshotProv)
|
if (m_snapshotProv)
|
||||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId);
|
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns);
|
||||||
else
|
else
|
||||||
m_lastResult = m_doc->compose(m_viewRootId);
|
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns);
|
||||||
|
|
||||||
s_composeDoc = nullptr;
|
s_composeDoc = nullptr;
|
||||||
|
|
||||||
@@ -569,9 +596,10 @@ void RcxController::refresh() {
|
|||||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||||
QSet<uint64_t> valid;
|
QSet<uint64_t> valid;
|
||||||
for (uint64_t id : m_selIds) {
|
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)
|
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;
|
m_selIds = valid;
|
||||||
|
|
||||||
@@ -805,6 +833,148 @@ void RcxController::deleteRootStruct(uint64_t structId) {
|
|||||||
if (!m_suppressRefresh) refresh();
|
if (!m_suppressRefresh) refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::groupIntoUnion(const QSet<uint64_t>& nodeIds) {
|
||||||
|
if (nodeIds.size() < 2) return;
|
||||||
|
|
||||||
|
// Collect nodes and verify they share the same parent
|
||||||
|
QVector<int> indices;
|
||||||
|
uint64_t parentId = 0;
|
||||||
|
bool first = true;
|
||||||
|
for (uint64_t id : nodeIds) {
|
||||||
|
int idx = m_doc->tree.indexOfId(id);
|
||||||
|
if (idx < 0) return;
|
||||||
|
if (first) { parentId = m_doc->tree.nodes[idx].parentId; first = false; }
|
||||||
|
else if (m_doc->tree.nodes[idx].parentId != parentId) return;
|
||||||
|
indices.append(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by offset to find the union's insertion point
|
||||||
|
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
|
||||||
|
return m_doc->tree.nodes[a].offset < m_doc->tree.nodes[b].offset;
|
||||||
|
});
|
||||||
|
int unionOffset = m_doc->tree.nodes[indices.first()].offset;
|
||||||
|
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Group into union"));
|
||||||
|
|
||||||
|
// Save copies of nodes before removal (subtrees included)
|
||||||
|
struct SavedNode { Node node; QVector<Node> subtree; };
|
||||||
|
QVector<SavedNode> saved;
|
||||||
|
for (int idx : indices) {
|
||||||
|
SavedNode sn;
|
||||||
|
sn.node = m_doc->tree.nodes[idx];
|
||||||
|
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
||||||
|
for (int si : sub)
|
||||||
|
if (si != idx) sn.subtree.append(m_doc->tree.nodes[si]);
|
||||||
|
saved.append(sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove selected nodes (in reverse order to keep indices valid)
|
||||||
|
for (int i = indices.size() - 1; i >= 0; i--) {
|
||||||
|
int idx = m_doc->tree.indexOfId(saved[i].node.id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
QVector<Node> subtree;
|
||||||
|
for (int si : m_doc->tree.subtreeIndices(saved[i].node.id))
|
||||||
|
subtree.append(m_doc->tree.nodes[si]);
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Remove{saved[i].node.id, subtree, {}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert union node
|
||||||
|
Node unionNode;
|
||||||
|
unionNode.kind = NodeKind::Struct;
|
||||||
|
unionNode.classKeyword = QStringLiteral("union");
|
||||||
|
unionNode.parentId = parentId;
|
||||||
|
unionNode.offset = unionOffset;
|
||||||
|
unionNode.id = m_doc->tree.reserveId();
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{unionNode}));
|
||||||
|
uint64_t unionId = unionNode.id;
|
||||||
|
|
||||||
|
// Re-insert nodes as children of the union, all at offset 0
|
||||||
|
for (const auto& sn : saved) {
|
||||||
|
Node copy = sn.node;
|
||||||
|
copy.parentId = unionId;
|
||||||
|
copy.offset = 0;
|
||||||
|
copy.id = m_doc->tree.reserveId();
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
||||||
|
|
||||||
|
// Re-insert subtree with updated parentId for direct children
|
||||||
|
uint64_t oldId = sn.node.id;
|
||||||
|
uint64_t newId = copy.id;
|
||||||
|
for (const auto& child : sn.subtree) {
|
||||||
|
Node cc = child;
|
||||||
|
if (cc.parentId == oldId) cc.parentId = newId;
|
||||||
|
cc.id = m_doc->tree.reserveId();
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
if (!m_suppressRefresh) refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcxController::dissolveUnion(uint64_t unionId) {
|
||||||
|
int ui = m_doc->tree.indexOfId(unionId);
|
||||||
|
if (ui < 0) return;
|
||||||
|
const Node& unionNode = m_doc->tree.nodes[ui];
|
||||||
|
if (unionNode.kind != NodeKind::Struct
|
||||||
|
|| unionNode.resolvedClassKeyword() != QStringLiteral("union")) return;
|
||||||
|
|
||||||
|
uint64_t parentId = unionNode.parentId;
|
||||||
|
int unionOffset = unionNode.offset;
|
||||||
|
|
||||||
|
// Collect union children
|
||||||
|
auto children = m_doc->tree.childrenOf(unionId);
|
||||||
|
struct SavedNode { Node node; QVector<Node> subtree; };
|
||||||
|
QVector<SavedNode> saved;
|
||||||
|
for (int ci : children) {
|
||||||
|
SavedNode sn;
|
||||||
|
sn.node = m_doc->tree.nodes[ci];
|
||||||
|
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
||||||
|
for (int si : sub)
|
||||||
|
if (si != ci) sn.subtree.append(m_doc->tree.nodes[si]);
|
||||||
|
saved.append(sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wasSuppressed = m_suppressRefresh;
|
||||||
|
m_suppressRefresh = true;
|
||||||
|
m_doc->undoStack.beginMacro(QStringLiteral("Dissolve union"));
|
||||||
|
|
||||||
|
// Remove the union (and all its children)
|
||||||
|
{
|
||||||
|
QVector<Node> subtree;
|
||||||
|
for (int si : m_doc->tree.subtreeIndices(unionId))
|
||||||
|
subtree.append(m_doc->tree.nodes[si]);
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
|
cmd::Remove{unionId, subtree, {}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-insert children under the union's parent, at the union's offset
|
||||||
|
for (const auto& sn : saved) {
|
||||||
|
Node copy = sn.node;
|
||||||
|
copy.parentId = parentId;
|
||||||
|
copy.offset = unionOffset + sn.node.offset;
|
||||||
|
copy.id = m_doc->tree.reserveId();
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
||||||
|
|
||||||
|
uint64_t oldId = sn.node.id;
|
||||||
|
uint64_t newId = copy.id;
|
||||||
|
for (const auto& child : sn.subtree) {
|
||||||
|
Node cc = child;
|
||||||
|
if (cc.parentId == oldId) cc.parentId = newId;
|
||||||
|
cc.id = m_doc->tree.reserveId();
|
||||||
|
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_doc->undoStack.endMacro();
|
||||||
|
m_suppressRefresh = wasSuppressed;
|
||||||
|
if (!m_suppressRefresh) refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::toggleCollapse(int nodeIdx) {
|
void RcxController::toggleCollapse(int nodeIdx) {
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
auto& node = m_doc->tree.nodes[nodeIdx];
|
auto& node = m_doc->tree.nodes[nodeIdx];
|
||||||
@@ -1003,6 +1173,10 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
m_valueHistory.remove(c.nodeId);
|
m_valueHistory.remove(c.nodeId);
|
||||||
for (int ci : tree.subtreeIndices(c.nodeId))
|
for (int ci : tree.subtreeIndices(c.nodeId))
|
||||||
m_valueHistory.remove(tree.nodes[ci].id);
|
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);
|
}, command);
|
||||||
|
|
||||||
@@ -1237,6 +1411,86 @@ void RcxController::splitHexNode(uint64_t nodeId) {
|
|||||||
refresh();
|
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,
|
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||||
int subLine, const QPoint& globalPos) {
|
int subLine, const QPoint& globalPos) {
|
||||||
auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); };
|
auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); };
|
||||||
@@ -1343,6 +1597,21 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
}
|
}
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// Check if all selected nodes share the same parent (required for grouping)
|
||||||
|
{
|
||||||
|
bool sameParent = true;
|
||||||
|
uint64_t firstParent = 0;
|
||||||
|
bool fp = true;
|
||||||
|
for (uint64_t id : ids) {
|
||||||
|
int idx = m_doc->tree.indexOfId(id);
|
||||||
|
if (idx < 0) { sameParent = false; break; }
|
||||||
|
if (fp) { firstParent = m_doc->tree.nodes[idx].parentId; fp = false; }
|
||||||
|
else if (m_doc->tree.nodes[idx].parentId != firstParent) { sameParent = false; break; }
|
||||||
|
}
|
||||||
|
if (sameParent)
|
||||||
|
menu.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
|
||||||
|
}
|
||||||
|
|
||||||
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
int idx = m_doc->tree.indexOfId(id);
|
int idx = m_doc->tree.indexOfId(id);
|
||||||
@@ -1378,6 +1647,31 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
uint64_t nodeId = node.id;
|
uint64_t nodeId = node.id;
|
||||||
uint64_t parentId = node.parentId;
|
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
|
// Quick-convert suggestions for Hex nodes
|
||||||
bool addedQuickConvert = false;
|
bool addedQuickConvert = false;
|
||||||
if (node.kind == NodeKind::Hex64) {
|
if (node.kind == NodeKind::Hex64) {
|
||||||
@@ -1551,6 +1845,26 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dissolve Union: available on union itself or any of its children
|
||||||
|
{
|
||||||
|
uint64_t targetUnionId = 0;
|
||||||
|
if (node.kind == NodeKind::Struct
|
||||||
|
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
|
||||||
|
targetUnionId = nodeId;
|
||||||
|
} else if (node.parentId != 0) {
|
||||||
|
int pi = m_doc->tree.indexOfId(node.parentId);
|
||||||
|
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
|
||||||
|
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
|
||||||
|
targetUnionId = node.parentId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetUnionId != 0) {
|
||||||
|
menu.addAction("Dissolve Union", [this, targetUnionId]() {
|
||||||
|
dissolveUnion(targetUnionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
|
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
|
||||||
int ni = m_doc->tree.indexOfId(nodeId);
|
int ni = m_doc->tree.indexOfId(nodeId);
|
||||||
if (ni >= 0) duplicateNode(ni);
|
if (ni >= 0) duplicateNode(ni);
|
||||||
@@ -1579,6 +1893,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
} // else (non-member node actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Always-available actions ──
|
// ── Always-available actions ──
|
||||||
@@ -1618,13 +1933,14 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
{
|
// Only add Track Value Changes here if not already added in node-specific section
|
||||||
|
if (!hasNode) {
|
||||||
auto* act = menu.addAction("Track Value Changes");
|
auto* act = menu.addAction("Track Value Changes");
|
||||||
act->setCheckable(true);
|
act->setCheckable(true);
|
||||||
act->setChecked(m_trackValues);
|
act->setChecked(m_trackValues);
|
||||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||||
|
menu.addSeparator();
|
||||||
}
|
}
|
||||||
menu.addSeparator();
|
|
||||||
|
|
||||||
menu.addAction(icon("arrow-left.svg"), "Undo", [this]() {
|
menu.addAction(icon("arrow-left.svg"), "Undo", [this]() {
|
||||||
m_doc->undoStack.undo();
|
m_doc->undoStack.undo();
|
||||||
@@ -1707,6 +2023,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
return nid | kFooterIdBit;
|
return nid | kFooterIdBit;
|
||||||
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
||||||
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
||||||
|
if (lm.isMemberLine && lm.subLine >= 0)
|
||||||
|
return makeMemberSelId(nid, lm.subLine);
|
||||||
return nid;
|
return nid;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1755,8 +2073,9 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
|
|
||||||
if (m_selIds.size() == 1) {
|
if (m_selIds.size() == 1) {
|
||||||
uint64_t sid = *m_selIds.begin();
|
uint64_t sid = *m_selIds.begin();
|
||||||
// Strip footer/array bits for node lookup
|
// Strip footer/array/member bits for node lookup
|
||||||
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
|
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||||
|
| kMemberBit | kMemberSubMask));
|
||||||
if (idx >= 0) emit nodeSelected(idx);
|
if (idx >= 0) emit nodeSelected(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1792,7 +2111,7 @@ void RcxController::updateCommandRow() {
|
|||||||
addr = QStringLiteral("0x") +
|
addr = QStringLiteral("0x") +
|
||||||
QString::number(m_doc->tree.baseAddress, 16).toUpper();
|
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));
|
.arg(elide(src, 40), elide(addr, 24));
|
||||||
|
|
||||||
// Build row 2: root class type + name (uses current view root)
|
// Build row 2: root class type + name (uses current view root)
|
||||||
@@ -1823,7 +2142,7 @@ void RcxController::updateCommandRow() {
|
|||||||
if (row2.isEmpty())
|
if (row2.isEmpty())
|
||||||
row2 = QStringLiteral("struct NoName {");
|
row2 = QStringLiteral("struct NoName {");
|
||||||
|
|
||||||
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2;
|
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2;
|
||||||
|
|
||||||
for (auto* ed : m_editors) {
|
for (auto* ed : m_editors) {
|
||||||
ed->setCommandRowText(combined);
|
ed->setCommandRowText(combined);
|
||||||
@@ -2522,6 +2841,11 @@ void RcxController::setRefreshInterval(int ms) {
|
|||||||
m_refreshTimer->setInterval(qMax(1, ms));
|
m_refreshTimer->setInterval(qMax(1, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::setCompactColumns(bool v) {
|
||||||
|
m_compactColumns = v;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::setupAutoRefresh() {
|
void RcxController::setupAutoRefresh() {
|
||||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
m_refreshTimer = new QTimer(this);
|
m_refreshTimer = new QTimer(this);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public:
|
|||||||
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||||
}
|
}
|
||||||
|
|
||||||
ComposeResult compose(uint64_t viewRootId = 0) const;
|
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
|
||||||
bool save(const QString& path);
|
bool save(const QString& path);
|
||||||
bool load(const QString& path);
|
bool load(const QString& path);
|
||||||
void loadData(const QString& binaryPath);
|
void loadData(const QString& binaryPath);
|
||||||
@@ -98,10 +98,14 @@ public:
|
|||||||
void duplicateNode(int nodeIdx);
|
void duplicateNode(int nodeIdx);
|
||||||
void convertToTypedPointer(uint64_t nodeId);
|
void convertToTypedPointer(uint64_t nodeId);
|
||||||
void splitHexNode(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 showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||||
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
||||||
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
||||||
void deleteRootStruct(uint64_t structId);
|
void deleteRootStruct(uint64_t structId);
|
||||||
|
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
|
||||||
|
void dissolveUnion(uint64_t unionId);
|
||||||
|
|
||||||
void applyCommand(const Command& cmd, bool isUndo);
|
void applyCommand(const Command& cmd, bool isUndo);
|
||||||
void refresh();
|
void refresh();
|
||||||
@@ -122,6 +126,7 @@ public:
|
|||||||
RcxDocument* document() const { return m_doc; }
|
RcxDocument* document() const { return m_doc; }
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
void setRefreshInterval(int ms);
|
void setRefreshInterval(int ms);
|
||||||
|
void setCompactColumns(bool v);
|
||||||
|
|
||||||
// MCP bridge accessors
|
// MCP bridge accessors
|
||||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||||
@@ -154,6 +159,7 @@ private:
|
|||||||
QSet<uint64_t> m_selIds;
|
QSet<uint64_t> m_selIds;
|
||||||
int m_anchorLine = -1;
|
int m_anchorLine = -1;
|
||||||
bool m_suppressRefresh = false;
|
bool m_suppressRefresh = false;
|
||||||
|
bool m_compactColumns = false;
|
||||||
uint64_t m_viewRootId = 0;
|
uint64_t m_viewRootId = 0;
|
||||||
|
|
||||||
// ── Saved sources for quick-switch ──
|
// ── Saved sources for quick-switch ──
|
||||||
|
|||||||
165
src/core.h
165
src/core.h
@@ -179,6 +179,14 @@ enum Marker : int {
|
|||||||
M_ACCENT = 9,
|
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 ──
|
// ── Node ──
|
||||||
|
|
||||||
struct Node {
|
struct Node {
|
||||||
@@ -196,6 +204,8 @@ struct Node {
|
|||||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||||
int viewIndex = 0; // Array: current view offset (transient)
|
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.
|
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||||
int byteSize() const {
|
int byteSize() const {
|
||||||
@@ -207,6 +217,12 @@ struct Node {
|
|||||||
if (elemSz <= 0) return 0;
|
if (elemSz <= 0) return 0;
|
||||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
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);
|
default: return sizeForKind(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +245,27 @@ struct Node {
|
|||||||
o["elementKind"] = kindToString(elementKind);
|
o["elementKind"] = kindToString(elementKind);
|
||||||
if (ptrDepth > 0)
|
if (ptrDepth > 0)
|
||||||
o["ptrDepth"] = ptrDepth;
|
o["ptrDepth"] = ptrDepth;
|
||||||
|
if (!enumMembers.isEmpty()) {
|
||||||
|
QJsonArray arr;
|
||||||
|
for (const auto& m : enumMembers) {
|
||||||
|
QJsonObject em;
|
||||||
|
em["name"] = m.first;
|
||||||
|
em["value"] = QString::number(m.second);
|
||||||
|
arr.append(em);
|
||||||
|
}
|
||||||
|
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;
|
return o;
|
||||||
}
|
}
|
||||||
static Node fromJson(const QJsonObject& o) {
|
static Node fromJson(const QJsonObject& o) {
|
||||||
@@ -246,6 +283,25 @@ struct Node {
|
|||||||
n.refId = o["refId"].toString("0").toULongLong();
|
n.refId = o["refId"].toString("0").toULongLong();
|
||||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||||
|
if (o.contains("enumMembers")) {
|
||||||
|
QJsonArray arr = o["enumMembers"].toArray();
|
||||||
|
for (const auto& v : arr) {
|
||||||
|
QJsonObject em = v.toObject();
|
||||||
|
n.enumMembers.append({em["name"].toString(),
|
||||||
|
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;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +549,18 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
|
|||||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
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 {
|
struct LineMeta {
|
||||||
int nodeIdx = -1;
|
int nodeIdx = -1;
|
||||||
uint64_t nodeId = 0;
|
uint64_t nodeId = 0;
|
||||||
@@ -522,6 +590,7 @@ struct LineMeta {
|
|||||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
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) {
|
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||||
@@ -566,13 +635,15 @@ namespace cmd {
|
|||||||
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
||||||
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
||||||
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
||||||
|
struct ChangeEnumMembers { uint64_t nodeId;
|
||||||
|
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||||
}
|
}
|
||||||
|
|
||||||
using Command = std::variant<
|
using Command = std::variant<
|
||||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||||
cmd::ChangeClassKeyword, cmd::ChangeOffset
|
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// ── Column spans (for inline editing) ──
|
// ── Column spans (for inline editing) ──
|
||||||
@@ -599,15 +670,16 @@ inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uin
|
|||||||
inline constexpr int kMaxTypeW = 128; // Maximum type column width
|
inline constexpr int kMaxTypeW = 128; // Maximum type column width
|
||||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||||
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
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) {
|
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;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
return {ind, ind + typeW, true};
|
return {ind, ind + typeW, true};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
|
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 ind = kFoldCol + lm.depth * 3;
|
||||||
int start = ind + typeW + kSepWidth;
|
int start = ind + typeW + kSepWidth;
|
||||||
@@ -622,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) {
|
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
|
||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
||||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||||
|
if (lm.isMemberLine) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
|
|
||||||
// Hex uses nameW for ASCII column (same as regular name column)
|
// Hex uses nameW for ASCII column (same as regular name column)
|
||||||
@@ -640,6 +713,27 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
|||||||
return {start, start + valWidth, true};
|
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) {
|
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
|
||||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
@@ -661,30 +755,14 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
|||||||
// Line format: "source▾ · 0x140000000"
|
// Line format: "source▾ · 0x140000000"
|
||||||
|
|
||||||
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
|
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
// Source label ends at the ▾ dropdown arrow
|
||||||
if (idx < 0) return {};
|
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||||
|
if (arrow < 0) return {};
|
||||||
int start = 0;
|
int start = 0;
|
||||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||||
if (start >= idx) return {};
|
if (start >= arrow) return {};
|
||||||
// Exclude trailing ▾ from the editable span
|
return {start, arrow, true};
|
||||||
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};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CommandRow root-class spans ──
|
// ── CommandRow root-class spans ──
|
||||||
@@ -703,6 +781,25 @@ inline int commandRowRootStart(const QString& lineText) {
|
|||||||
return best;
|
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) {
|
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
|
||||||
int start = commandRowRootStart(lineText);
|
int start = commandRowRootStart(lineText);
|
||||||
if (start < 0) return {};
|
if (start < 0) return {};
|
||||||
@@ -851,17 +948,18 @@ namespace fmt {
|
|||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine = 0,
|
uint64_t addr, int depth, int subLine = 0,
|
||||||
const QString& comment = {}, int colType = kColType, int colName = kColName,
|
const QString& comment = {}, int colType = kColType, int colName = kColName,
|
||||||
const QString& typeOverride = {});
|
const QString& typeOverride = {}, bool compact = false);
|
||||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
|
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
|
||||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
|
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
|
||||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {});
|
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
|
||||||
QString structTypeName(const Node& node); // Full type string for struct headers
|
QString structTypeName(const Node& node); // Full type string for struct headers
|
||||||
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
|
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
|
||||||
QString pointerTypeName(NodeKind kind, const QString& targetName);
|
QString pointerTypeName(NodeKind kind, const QString& targetName);
|
||||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||||
const Provider& prov, uint64_t addr,
|
const Provider& prov, uint64_t addr,
|
||||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
|
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
|
||||||
|
bool compact = false);
|
||||||
QString validateBaseAddress(const QString& text);
|
QString validateBaseAddress(const QString& text);
|
||||||
QString indent(int depth);
|
QString indent(int depth);
|
||||||
QString readValue(const Node& node, const Provider& prov,
|
QString readValue(const Node& node, const Provider& prov,
|
||||||
@@ -871,10 +969,17 @@ namespace fmt {
|
|||||||
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
||||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||||
QString validateValue(NodeKind kind, const QString& text);
|
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
|
} // namespace fmt
|
||||||
|
|
||||||
// ── Compose function forward declaration ──
|
// ── Compose function forward declaration ──
|
||||||
|
|
||||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||||
|
bool compactColumns = false);
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -880,7 +880,7 @@ void RcxEditor::reformatMargins() {
|
|||||||
for (int i = 0; i < m_meta.size(); i++) {
|
for (int i = 0; i < m_meta.size(); i++) {
|
||||||
auto& lm = m_meta[i];
|
auto& lm = m_meta[i];
|
||||||
|
|
||||||
if (lm.isContinuation) {
|
if (lm.isContinuation || lm.isMemberLine) {
|
||||||
lm.offsetText = QStringLiteral(" \u00B7 ");
|
lm.offsetText = QStringLiteral(" \u00B7 ");
|
||||||
} else if (lm.offsetText.isEmpty()) {
|
} else if (lm.offsetText.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1079,8 +1079,11 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
for (uint64_t selId : selIds) {
|
for (uint64_t selId : selIds) {
|
||||||
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
||||||
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
||||||
|
bool isMemberSel = (selId & kMemberBit) != 0;
|
||||||
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
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);
|
auto it = m_nodeLineIndex.constFind(nodeId);
|
||||||
if (it == m_nodeLineIndex.constEnd()) continue;
|
if (it == m_nodeLineIndex.constEnd()) continue;
|
||||||
for (int ln : *it) {
|
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)
|
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
||||||
continue;
|
continue;
|
||||||
} else if (m_meta[ln].isArrayElement) {
|
} else if (m_meta[ln].isArrayElement) {
|
||||||
// Plain nodeId selection shouldn't highlight individual array elements
|
continue;
|
||||||
// (the header line is enough)
|
}
|
||||||
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
m_sci->markerAdd(ln, M_SELECTED);
|
m_sci->markerAdd(ln, M_SELECTED);
|
||||||
@@ -1127,7 +1135,8 @@ void RcxEditor::applyHoverHighlight() {
|
|||||||
if (prevId != 0) {
|
if (prevId != 0) {
|
||||||
// Check if old hovered line was a single-line highlight (footer or array element)
|
// Check if old hovered line was a single-line highlight (footer or array element)
|
||||||
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
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) {
|
if (prevSingleLine) {
|
||||||
m_sci->markerDelete(prevLine, M_HOVER);
|
m_sci->markerDelete(prevLine, M_HOVER);
|
||||||
} else {
|
} else {
|
||||||
@@ -1143,11 +1152,13 @@ void RcxEditor::applyHoverHighlight() {
|
|||||||
if (!m_hoverInside) return;
|
if (!m_hoverInside) return;
|
||||||
if (m_hoveredNodeId == 0) 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() &&
|
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||||
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||||
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||||
m_meta[m_hoveredLine].isArrayElement);
|
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)
|
// Check if the hovered item is already selected (using appropriate ID)
|
||||||
uint64_t checkId;
|
uint64_t checkId;
|
||||||
@@ -1155,12 +1166,14 @@ void RcxEditor::applyHoverHighlight() {
|
|||||||
checkId = m_hoveredNodeId | kFooterIdBit;
|
checkId = m_hoveredNodeId | kFooterIdBit;
|
||||||
else if (hoveringArrayElem)
|
else if (hoveringArrayElem)
|
||||||
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
||||||
|
else if (hoveringMember)
|
||||||
|
checkId = makeMemberSelId(m_hoveredNodeId, m_meta[m_hoveredLine].subLine);
|
||||||
else
|
else
|
||||||
checkId = m_hoveredNodeId;
|
checkId = m_hoveredNodeId;
|
||||||
if (m_currentSelIds.contains(checkId)) return;
|
if (m_currentSelIds.contains(checkId)) return;
|
||||||
|
|
||||||
if (hoveringFooter || hoveringArrayElem) {
|
if (hoveringFooter || hoveringArrayElem || hoveringMember) {
|
||||||
// Single-line highlight for footers and array elements
|
// Single-line highlight for footers, array elements, and member lines
|
||||||
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||||
} else {
|
} else {
|
||||||
// Non-footer, non-array-element: highlight all lines for this node
|
// 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))
|
if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart))
|
||||||
fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1);
|
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
|
// Dim base address to match source/struct grey
|
||||||
ColumnSpan addrSpan = commandRowAddrSpan(t);
|
ColumnSpan addrSpan = commandRowAddrSpan(t);
|
||||||
if (addrSpan.valid)
|
if (addrSpan.valid)
|
||||||
@@ -1615,6 +1619,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
|||||||
if (!s.valid && t == EditTarget::Name)
|
if (!s.valid && t == EditTarget::Name)
|
||||||
s = headerNameSpan(*lm, lineText);
|
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);
|
out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true);
|
||||||
if (lineTextOut) *lineTextOut = lineText;
|
if (lineTextOut) *lineTextOut = lineText;
|
||||||
return out.valid;
|
return out.valid;
|
||||||
@@ -1728,6 +1738,12 @@ static bool hitTestTarget(QsciScintilla* sci,
|
|||||||
if (!ns.valid)
|
if (!ns.valid)
|
||||||
ns = headerNameSpan(lm, lineText);
|
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;
|
if (inSpan(ts)) outTarget = EditTarget::Type;
|
||||||
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
else if (inSpan(ns)) outTarget = EditTarget::Name;
|
||||||
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
else if (inSpan(vs)) outTarget = EditTarget::Value;
|
||||||
@@ -2686,6 +2702,8 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
checkId = lm->nodeId | kFooterIdBit;
|
checkId = lm->nodeId | kFooterIdBit;
|
||||||
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
||||||
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
||||||
|
else if (lm->isMemberLine && lm->subLine >= 0)
|
||||||
|
checkId = makeMemberSelId(lm->nodeId, lm->subLine);
|
||||||
else
|
else
|
||||||
checkId = lm->nodeId;
|
checkId = lm->nodeId;
|
||||||
return m_currentSelIds.contains(checkId);
|
return m_currentSelIds.contains(checkId);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ public:
|
|||||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
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
|
// Saved sources for quick-switch in source picker
|
||||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||||
|
|
||||||
|
|||||||
356
src/examples/EPROCESS.rcx
Normal file
356
src/examples/EPROCESS.rcx
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
{
|
||||||
|
"baseAddress": "FFFF800000000000",
|
||||||
|
"nextId": "9000",
|
||||||
|
"nodes": [
|
||||||
|
{"id":"100","kind":"Struct","name":"list_entry","structTypeName":"_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"101","kind":"Pointer64","name":"Flink","offset":0,"parentId":"100","refId":"100","collapsed":true},
|
||||||
|
{"id":"102","kind":"Pointer64","name":"Blink","offset":8,"parentId":"100","refId":"100","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"110","kind":"Struct","name":"single_list_entry","structTypeName":"_SINGLE_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"111","kind":"Pointer64","name":"Next","offset":0,"parentId":"110","refId":"110","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"120","kind":"Struct","name":"ex_push_lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"121","kind":"Hex64","name":"Value","offset":0,"parentId":"120"},
|
||||||
|
|
||||||
|
{"id":"130","kind":"Struct","name":"ex_rundown_ref","structTypeName":"_EX_RUNDOWN_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"131","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"130","refId":"0","collapsed":false},
|
||||||
|
{"id":"132","kind":"UInt64","name":"Count","offset":0,"parentId":"131"},
|
||||||
|
{"id":"133","kind":"Pointer64","name":"Ptr","offset":0,"parentId":"131"},
|
||||||
|
|
||||||
|
{"id":"140","kind":"Struct","name":"ex_fast_ref","structTypeName":"_EX_FAST_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"141","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"140","refId":"0","collapsed":false},
|
||||||
|
{"id":"142","kind":"Pointer64","name":"Object","offset":0,"parentId":"141"},
|
||||||
|
{"id":"143","kind":"UInt64","name":"Value","offset":0,"parentId":"141"},
|
||||||
|
|
||||||
|
{"id":"150","kind":"Struct","name":"unicode_string","structTypeName":"_UNICODE_STRING","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"151","kind":"UInt16","name":"Length","offset":0,"parentId":"150"},
|
||||||
|
{"id":"152","kind":"UInt16","name":"MaximumLength","offset":2,"parentId":"150"},
|
||||||
|
{"id":"153","kind":"Pointer64","name":"Buffer","offset":8,"parentId":"150"},
|
||||||
|
|
||||||
|
{"id":"160","kind":"Struct","name":"large_integer","structTypeName":"_LARGE_INTEGER","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"161","kind":"Struct","name":"","offset":0,"parentId":"160","refId":"0","collapsed":false},
|
||||||
|
{"id":"162","kind":"UInt32","name":"LowPart","offset":0,"parentId":"161"},
|
||||||
|
{"id":"163","kind":"Int32","name":"HighPart","offset":4,"parentId":"161"},
|
||||||
|
{"id":"164","kind":"Int64","name":"QuadPart","offset":0,"parentId":"160"},
|
||||||
|
|
||||||
|
{"id":"170","kind":"Struct","name":"rtl_avl_tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"171","kind":"Pointer64","name":"Root","offset":0,"parentId":"170"},
|
||||||
|
|
||||||
|
{"id":"180","kind":"Struct","name":"kstack_count","structTypeName":"_KSTACK_COUNT","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"181","kind":"Int32","name":"Value","offset":0,"parentId":"180"},
|
||||||
|
{"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":"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"},
|
||||||
|
|
||||||
|
{"id":"210","kind":"Struct","name":"ps_protection","structTypeName":"_PS_PROTECTION","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"211","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"210","refId":"0","collapsed":false},
|
||||||
|
{"id":"212","kind":"UInt8","name":"Level","offset":0,"parentId":"211"},
|
||||||
|
{"id":"213","kind":"Hex8","name":"Type:3 Audit:1 Signer:4","offset":0,"parentId":"211"},
|
||||||
|
|
||||||
|
{"id":"220","kind":"Struct","name":"timer_delay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"221","kind":"Hex64","name":"DelayMs:30 CoalescingWindowMs:30 Reserved:1 NewTimerWheel:1 Retry:1 Locked:1","offset":0,"parentId":"220"},
|
||||||
|
{"id":"222","kind":"UInt64","name":"All","offset":0,"parentId":"220"},
|
||||||
|
|
||||||
|
{"id":"230","kind":"Struct","name":"wnf_state_name","structTypeName":"_WNF_STATE_NAME","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"231","kind":"UInt32","name":"Data_0","offset":0,"parentId":"230"},
|
||||||
|
{"id":"232","kind":"UInt32","name":"Data_1","offset":4,"parentId":"230"},
|
||||||
|
|
||||||
|
{"id":"240","kind":"Struct","name":"dynamic_ranges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"241","kind":"Struct","name":"Tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"240","refId":"170","collapsed":true},
|
||||||
|
{"id":"242","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":8,"parentId":"240","refId":"120","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"250","kind":"Struct","name":"alpc_context","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"251","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"250","refId":"120","collapsed":true},
|
||||||
|
{"id":"252","kind":"Struct","name":"ViewListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"250","refId":"100","collapsed":true},
|
||||||
|
{"id":"253","kind":"UInt64","name":"PagedPoolQuotaCache","offset":24,"parentId":"250"},
|
||||||
|
|
||||||
|
{"id":"260","kind":"Struct","name":"mmsupport_flags","structTypeName":"_MMSUPPORT_FLAGS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"261","kind":"Hex32","name":"EntireFlags","offset":0,"parentId":"260"},
|
||||||
|
|
||||||
|
{"id":"270","kind":"Struct","name":"mmsupport_shared","structTypeName":"_MMSUPPORT_SHARED","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"271","kind":"Pointer64","name":"WorkingSetLockArray","offset":0,"parentId":"270"},
|
||||||
|
{"id":"272","kind":"UInt64","name":"ReleasedCommitDebt","offset":8,"parentId":"270"},
|
||||||
|
{"id":"273","kind":"UInt64","name":"ResetPagesRepurposedCount","offset":16,"parentId":"270"},
|
||||||
|
{"id":"274","kind":"Pointer64","name":"WsSwapSupport","offset":24,"parentId":"270"},
|
||||||
|
{"id":"275","kind":"Pointer64","name":"CommitReleaseContext","offset":32,"parentId":"270"},
|
||||||
|
{"id":"276","kind":"Pointer64","name":"AccessLog","offset":40,"parentId":"270"},
|
||||||
|
{"id":"277","kind":"UInt64","name":"ChargedWslePages","offset":48,"parentId":"270"},
|
||||||
|
{"id":"278","kind":"UInt64","name":"ActualWslePages","offset":56,"parentId":"270"},
|
||||||
|
{"id":"279","kind":"Int32","name":"WorkingSetCoreLock","offset":64,"parentId":"270"},
|
||||||
|
{"id":"280","kind":"Pointer64","name":"ShadowMapping","offset":72,"parentId":"270"},
|
||||||
|
|
||||||
|
{"id":"300","kind":"Struct","name":"mmsupport_instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"301","kind":"UInt32","name":"NextPageColor","offset":0,"parentId":"300"},
|
||||||
|
{"id":"302","kind":"UInt32","name":"PageFaultCount","offset":4,"parentId":"300"},
|
||||||
|
{"id":"303","kind":"UInt64","name":"TrimmedPageCount","offset":8,"parentId":"300"},
|
||||||
|
{"id":"304","kind":"Pointer64","name":"VmWorkingSetList","offset":16,"parentId":"300"},
|
||||||
|
{"id":"305","kind":"Struct","name":"WorkingSetExpansionLinks","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"300","refId":"100","collapsed":true},
|
||||||
|
{"id":"306","kind":"UInt64","name":"AgeDistribution_0","offset":40,"parentId":"300"},
|
||||||
|
{"id":"307","kind":"UInt64","name":"AgeDistribution_1","offset":48,"parentId":"300"},
|
||||||
|
{"id":"308","kind":"UInt64","name":"AgeDistribution_2","offset":56,"parentId":"300"},
|
||||||
|
{"id":"309","kind":"UInt64","name":"AgeDistribution_3","offset":64,"parentId":"300"},
|
||||||
|
{"id":"310","kind":"UInt64","name":"AgeDistribution_4","offset":72,"parentId":"300"},
|
||||||
|
{"id":"311","kind":"UInt64","name":"AgeDistribution_5","offset":80,"parentId":"300"},
|
||||||
|
{"id":"312","kind":"UInt64","name":"AgeDistribution_6","offset":88,"parentId":"300"},
|
||||||
|
{"id":"313","kind":"UInt64","name":"AgeDistribution_7","offset":96,"parentId":"300"},
|
||||||
|
{"id":"314","kind":"Pointer64","name":"ExitOutswapGate","offset":104,"parentId":"300"},
|
||||||
|
{"id":"315","kind":"UInt64","name":"MinimumWorkingSetSize","offset":112,"parentId":"300"},
|
||||||
|
{"id":"316","kind":"UInt64","name":"MaximumWorkingSetSize","offset":120,"parentId":"300"},
|
||||||
|
{"id":"317","kind":"UInt64","name":"WorkingSetLeafSize","offset":128,"parentId":"300"},
|
||||||
|
{"id":"318","kind":"UInt64","name":"WorkingSetLeafPrivateSize","offset":136,"parentId":"300"},
|
||||||
|
{"id":"319","kind":"UInt64","name":"WorkingSetSize","offset":144,"parentId":"300"},
|
||||||
|
{"id":"320","kind":"UInt64","name":"WorkingSetPrivateSize","offset":152,"parentId":"300"},
|
||||||
|
{"id":"321","kind":"UInt64","name":"PeakWorkingSetSize","offset":160,"parentId":"300"},
|
||||||
|
{"id":"322","kind":"UInt32","name":"HardFaultCount","offset":168,"parentId":"300"},
|
||||||
|
{"id":"323","kind":"UInt16","name":"LastTrimStamp","offset":172,"parentId":"300"},
|
||||||
|
{"id":"324","kind":"UInt16","name":"PartitionId","offset":174,"parentId":"300"},
|
||||||
|
{"id":"325","kind":"UInt64","name":"SelfmapLock","offset":176,"parentId":"300"},
|
||||||
|
{"id":"326","kind":"Struct","name":"Flags","structTypeName":"_MMSUPPORT_FLAGS","offset":184,"parentId":"300","refId":"260","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"350","kind":"Struct","name":"mmsupport_full","structTypeName":"_MMSUPPORT_FULL","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"351","kind":"Struct","name":"Instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"350","refId":"300","collapsed":true},
|
||||||
|
{"id":"352","kind":"Struct","name":"Shared","structTypeName":"_MMSUPPORT_SHARED","offset":192,"parentId":"350","refId":"270","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"400","kind":"Struct","name":"dispatcher_header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"401","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"400","refId":"0","collapsed":false},
|
||||||
|
{"id":"402","kind":"UInt8","name":"Type","offset":0,"parentId":"401"},
|
||||||
|
{"id":"403","kind":"UInt8","name":"Signalling","offset":1,"parentId":"401"},
|
||||||
|
{"id":"404","kind":"UInt8","name":"Size","offset":2,"parentId":"401"},
|
||||||
|
{"id":"405","kind":"UInt8","name":"Reserved1","offset":3,"parentId":"401"},
|
||||||
|
{"id":"406","kind":"Int32","name":"Lock","offset":0,"parentId":"401"},
|
||||||
|
{"id":"407","kind":"Int32","name":"SignalState","offset":4,"parentId":"400"},
|
||||||
|
{"id":"408","kind":"Struct","name":"WaitListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"400","refId":"100","collapsed":true},
|
||||||
|
|
||||||
|
{"id":"500","kind":"Struct","name":"kprocess","structTypeName":"_KPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||||
|
{"id":"501","kind":"Struct","name":"Header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"500","refId":"400","collapsed":true},
|
||||||
|
{"id":"502","kind":"Struct","name":"ProfileListHead","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"500","refId":"100","collapsed":true},
|
||||||
|
{"id":"503","kind":"UInt64","name":"DirectoryTableBase","offset":40,"parentId":"500"},
|
||||||
|
{"id":"504","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":48,"parentId":"500","refId":"100","collapsed":true},
|
||||||
|
{"id":"505","kind":"UInt32","name":"ProcessLock","offset":64,"parentId":"500"},
|
||||||
|
{"id":"506","kind":"UInt32","name":"ProcessTimerDelay","offset":68,"parentId":"500"},
|
||||||
|
{"id":"507","kind":"UInt64","name":"DeepFreezeStartTime","offset":72,"parentId":"500"},
|
||||||
|
{"id":"508","kind":"Pointer64","name":"Affinity","offset":80,"parentId":"500"},
|
||||||
|
{"id":"509","kind":"Hex64","name":"AutoBoostState","offset":88,"parentId":"500"},
|
||||||
|
{"id":"510","kind":"Struct","name":"ReadyListHead","structTypeName":"_LIST_ENTRY","offset":104,"parentId":"500","refId":"100","collapsed":true},
|
||||||
|
{"id":"511","kind":"Struct","name":"SwapListEntry","structTypeName":"_SINGLE_LIST_ENTRY","offset":120,"parentId":"500","refId":"110","collapsed":true},
|
||||||
|
{"id":"512","kind":"Pointer64","name":"ActiveProcessors","offset":128,"parentId":"500"},
|
||||||
|
{"id":"513","kind":"Struct","name":"","classKeyword":"union","offset":136,"parentId":"500","refId":"0","collapsed":false},
|
||||||
|
{"id":"514","kind":"Hex32","name":"AutoAlignment:1 DisableBoost:1 DisableQuantum:1 DeepFreeze:1 TimerVirtualization:1 CheckStackExtents:1 CacheIsolationEnabled:1 PpmPolicy:4 VaSpaceDeleted:1 MultiGroup:1 ForegroundProcess:1 ReservedFlags:18","offset":0,"parentId":"513"},
|
||||||
|
{"id":"515","kind":"Int32","name":"ProcessFlags","offset":0,"parentId":"513"},
|
||||||
|
{"id":"516","kind":"Int8","name":"BasePriority","offset":144,"parentId":"500"},
|
||||||
|
{"id":"517","kind":"Int8","name":"QuantumReset","offset":145,"parentId":"500"},
|
||||||
|
{"id":"518","kind":"Int8","name":"Visited","offset":146,"parentId":"500"},
|
||||||
|
{"id":"519","kind":"Struct","name":"Flags","structTypeName":"_KEXECUTE_OPTIONS","offset":147,"parentId":"500","refId":"190","collapsed":true},
|
||||||
|
{"id":"520","kind":"Struct","name":"StackCount","structTypeName":"_KSTACK_COUNT","offset":264,"parentId":"500","refId":"180","collapsed":true},
|
||||||
|
{"id":"521","kind":"Struct","name":"ProcessListEntry","structTypeName":"_LIST_ENTRY","offset":272,"parentId":"500","refId":"100","collapsed":true},
|
||||||
|
{"id":"522","kind":"UInt64","name":"CycleTime","offset":288,"parentId":"500"},
|
||||||
|
{"id":"523","kind":"UInt64","name":"ContextSwitches","offset":296,"parentId":"500"},
|
||||||
|
{"id":"524","kind":"Pointer64","name":"SchedulingGroup","offset":304,"parentId":"500"},
|
||||||
|
{"id":"525","kind":"UInt64","name":"KernelTime","offset":312,"parentId":"500"},
|
||||||
|
{"id":"526","kind":"UInt64","name":"UserTime","offset":320,"parentId":"500"},
|
||||||
|
{"id":"527","kind":"UInt64","name":"ReadyTime","offset":328,"parentId":"500"},
|
||||||
|
{"id":"528","kind":"UInt32","name":"FreezeCount","offset":336,"parentId":"500"},
|
||||||
|
{"id":"529","kind":"UInt64","name":"UserDirectoryTableBase","offset":344,"parentId":"500"},
|
||||||
|
{"id":"530","kind":"UInt8","name":"AddressPolicy","offset":352,"parentId":"500"},
|
||||||
|
{"id":"531","kind":"Pointer64","name":"InstrumentationCallback","offset":360,"parentId":"500"},
|
||||||
|
{"id":"532","kind":"UInt64","name":"SecureHandle","offset":368,"parentId":"500"},
|
||||||
|
{"id":"533","kind":"UInt64","name":"KernelWaitTime","offset":376,"parentId":"500"},
|
||||||
|
{"id":"534","kind":"UInt64","name":"UserWaitTime","offset":384,"parentId":"500"},
|
||||||
|
{"id":"535","kind":"UInt64","name":"LastRebalanceQpc","offset":392,"parentId":"500"},
|
||||||
|
{"id":"536","kind":"Pointer64","name":"PerProcessorCycleTimes","offset":400,"parentId":"500"},
|
||||||
|
{"id":"537","kind":"UInt64","name":"ExtendedFeatureDisableMask","offset":408,"parentId":"500"},
|
||||||
|
{"id":"538","kind":"UInt16","name":"PrimaryGroup","offset":416,"parentId":"500"},
|
||||||
|
{"id":"539","kind":"Pointer64","name":"UserCetLogging","offset":424,"parentId":"500"},
|
||||||
|
{"id":"540","kind":"Struct","name":"CpuPartitionList","structTypeName":"_LIST_ENTRY","offset":432,"parentId":"500","refId":"100","collapsed":true},
|
||||||
|
{"id":"541","kind":"Pointer64","name":"AvailableCpuState","offset":448,"parentId":"500"},
|
||||||
|
|
||||||
|
{"id":"2000","kind":"Struct","name":"eprocess","structTypeName":"_EPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":false},
|
||||||
|
{"id":"2001","kind":"Struct","name":"Pcb","structTypeName":"_KPROCESS","offset":0,"parentId":"2000","refId":"500","collapsed":true},
|
||||||
|
{"id":"2002","kind":"Struct","name":"ProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":456,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2003","kind":"Pointer64","name":"UniqueProcessId","offset":464,"parentId":"2000"},
|
||||||
|
{"id":"2004","kind":"Struct","name":"ActiveProcessLinks","structTypeName":"_LIST_ENTRY","offset":472,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2005","kind":"Struct","name":"RundownProtect","structTypeName":"_EX_RUNDOWN_REF","offset":488,"parentId":"2000","refId":"130","collapsed":true},
|
||||||
|
{"id":"2006","kind":"Struct","name":"","classKeyword":"union","offset":496,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2007","kind":"UInt32","name":"Flags2","offset":0,"parentId":"2006"},
|
||||||
|
{"id":"2008","kind":"Hex32","name":"JobNotReallyActive:1 AccountingFolded:1 NewProcessReported:1 ExitProcessReported:1 ReportCommitChanges:1 LastReportMemory:1 ForceWakeCharge:1 CrossSessionCreate:1 NeedsHandleRundown:1 RefTraceEnabled:1 PicoCreated:1 EmptyJobEvaluated:1 DefaultPagePriority:3 PrimaryTokenFrozen:1","offset":0,"parentId":"2006"},
|
||||||
|
{"id":"2009","kind":"Struct","name":"","classKeyword":"union","offset":500,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2010","kind":"UInt32","name":"Flags","offset":0,"parentId":"2009"},
|
||||||
|
{"id":"2011","kind":"Hex32","name":"CreateReported:1 NoDebugInherit:1 ProcessExiting:1 ProcessDelete:1 ManageExecutableMemoryWrites:1 VmDeleted:1 OutswapEnabled:1 Outswapped:1 FailFastOnCommitFail:1 Wow64VaSpace4Gb:1 AddressSpaceInitialized:2 SetTimerResolution:1 BreakOnTermination:1","offset":0,"parentId":"2009"},
|
||||||
|
{"id":"2012","kind":"Int64","name":"CreateTime","offset":504,"parentId":"2000"},
|
||||||
|
{"id":"2013","kind":"UInt64","name":"ProcessQuotaUsage_0","offset":512,"parentId":"2000"},
|
||||||
|
{"id":"2014","kind":"UInt64","name":"ProcessQuotaUsage_1","offset":520,"parentId":"2000"},
|
||||||
|
{"id":"2015","kind":"UInt64","name":"ProcessQuotaPeak_0","offset":528,"parentId":"2000"},
|
||||||
|
{"id":"2016","kind":"UInt64","name":"ProcessQuotaPeak_1","offset":536,"parentId":"2000"},
|
||||||
|
{"id":"2017","kind":"UInt64","name":"PeakVirtualSize","offset":544,"parentId":"2000"},
|
||||||
|
{"id":"2018","kind":"UInt64","name":"VirtualSize","offset":552,"parentId":"2000"},
|
||||||
|
{"id":"2019","kind":"Struct","name":"SessionProcessLinks","structTypeName":"_LIST_ENTRY","offset":560,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2020","kind":"Struct","name":"","classKeyword":"union","offset":576,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2021","kind":"Pointer64","name":"ExceptionPortData","offset":0,"parentId":"2020"},
|
||||||
|
{"id":"2022","kind":"UInt64","name":"ExceptionPortValue","offset":0,"parentId":"2020"},
|
||||||
|
{"id":"2023","kind":"Struct","name":"Token","structTypeName":"_EX_FAST_REF","offset":584,"parentId":"2000","refId":"140","collapsed":true},
|
||||||
|
{"id":"2024","kind":"UInt64","name":"MmReserved","offset":592,"parentId":"2000"},
|
||||||
|
{"id":"2025","kind":"Struct","name":"AddressCreationLock","structTypeName":"_EX_PUSH_LOCK","offset":600,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2026","kind":"Struct","name":"PageTableCommitmentLock","structTypeName":"_EX_PUSH_LOCK","offset":608,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2027","kind":"Pointer64","name":"RotateInProgress","offset":616,"parentId":"2000"},
|
||||||
|
{"id":"2028","kind":"Pointer64","name":"ForkInProgress","offset":624,"parentId":"2000"},
|
||||||
|
{"id":"2029","kind":"Pointer64","name":"CommitChargeJob","offset":632,"parentId":"2000"},
|
||||||
|
{"id":"2030","kind":"Struct","name":"CloneRoot","structTypeName":"_RTL_AVL_TREE","offset":640,"parentId":"2000","refId":"170","collapsed":true},
|
||||||
|
{"id":"2031","kind":"UInt64","name":"NumberOfPrivatePages","offset":648,"parentId":"2000"},
|
||||||
|
{"id":"2032","kind":"UInt64","name":"NumberOfLockedPages","offset":656,"parentId":"2000"},
|
||||||
|
{"id":"2033","kind":"Pointer64","name":"Win32Process","offset":664,"parentId":"2000"},
|
||||||
|
{"id":"2034","kind":"Pointer64","name":"Job","offset":672,"parentId":"2000"},
|
||||||
|
{"id":"2035","kind":"Pointer64","name":"SectionObject","offset":680,"parentId":"2000"},
|
||||||
|
{"id":"2036","kind":"Pointer64","name":"SectionBaseAddress","offset":688,"parentId":"2000"},
|
||||||
|
{"id":"2037","kind":"UInt32","name":"Cookie","offset":696,"parentId":"2000"},
|
||||||
|
{"id":"2038","kind":"Pointer64","name":"WorkingSetWatch","offset":704,"parentId":"2000"},
|
||||||
|
{"id":"2039","kind":"Pointer64","name":"Win32WindowStation","offset":712,"parentId":"2000"},
|
||||||
|
{"id":"2040","kind":"Pointer64","name":"InheritedFromUniqueProcessId","offset":720,"parentId":"2000"},
|
||||||
|
{"id":"2041","kind":"UInt64","name":"OwnerProcessId","offset":728,"parentId":"2000"},
|
||||||
|
{"id":"2042","kind":"Pointer64","name":"Peb","offset":736,"parentId":"2000"},
|
||||||
|
{"id":"2043","kind":"Pointer64","name":"Session","offset":744,"parentId":"2000"},
|
||||||
|
{"id":"2044","kind":"Pointer64","name":"Spare1","offset":752,"parentId":"2000"},
|
||||||
|
{"id":"2045","kind":"Pointer64","name":"QuotaBlock","offset":760,"parentId":"2000"},
|
||||||
|
{"id":"2046","kind":"Pointer64","name":"ObjectTable","offset":768,"parentId":"2000"},
|
||||||
|
{"id":"2047","kind":"Pointer64","name":"DebugPort","offset":776,"parentId":"2000"},
|
||||||
|
{"id":"2048","kind":"Pointer64","name":"WoW64Process","offset":784,"parentId":"2000"},
|
||||||
|
{"id":"2049","kind":"Struct","name":"DeviceMap","structTypeName":"_EX_FAST_REF","offset":792,"parentId":"2000","refId":"140","collapsed":true},
|
||||||
|
{"id":"2050","kind":"Pointer64","name":"EtwDataSource","offset":800,"parentId":"2000"},
|
||||||
|
{"id":"2051","kind":"UInt64","name":"PageDirectoryPte","offset":808,"parentId":"2000"},
|
||||||
|
{"id":"2052","kind":"Pointer64","name":"ImageFilePointer","offset":816,"parentId":"2000"},
|
||||||
|
{"id":"2053","kind":"Hex64","name":"ImageFileName_lo","offset":824,"parentId":"2000"},
|
||||||
|
{"id":"2054","kind":"Hex32","name":"ImageFileName_mi","offset":832,"parentId":"2000"},
|
||||||
|
{"id":"2055","kind":"Hex16","name":"ImageFileName_hi","offset":836,"parentId":"2000"},
|
||||||
|
{"id":"2056","kind":"UInt8","name":"ImageFileName_14","offset":838,"parentId":"2000"},
|
||||||
|
{"id":"2057","kind":"UInt8","name":"PriorityClass","offset":839,"parentId":"2000"},
|
||||||
|
{"id":"2058","kind":"Pointer64","name":"SecurityPort","offset":840,"parentId":"2000"},
|
||||||
|
{"id":"2059","kind":"Struct","name":"SeAuditProcessCreationInfo","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":848,"parentId":"2000","refId":"200","collapsed":true},
|
||||||
|
{"id":"2060","kind":"Struct","name":"JobLinks","structTypeName":"_LIST_ENTRY","offset":856,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2061","kind":"Pointer64","name":"HighestUserAddress","offset":872,"parentId":"2000"},
|
||||||
|
{"id":"2062","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":880,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2063","kind":"UInt32","name":"ActiveThreads","offset":896,"parentId":"2000"},
|
||||||
|
{"id":"2064","kind":"UInt32","name":"ImagePathHash","offset":900,"parentId":"2000"},
|
||||||
|
{"id":"2065","kind":"UInt32","name":"DefaultHardErrorProcessing","offset":904,"parentId":"2000"},
|
||||||
|
{"id":"2066","kind":"Int32","name":"LastThreadExitStatus","offset":908,"parentId":"2000"},
|
||||||
|
{"id":"2067","kind":"Struct","name":"PrefetchTrace","structTypeName":"_EX_FAST_REF","offset":912,"parentId":"2000","refId":"140","collapsed":true},
|
||||||
|
{"id":"2068","kind":"Pointer64","name":"LockedPagesList","offset":920,"parentId":"2000"},
|
||||||
|
{"id":"2069","kind":"Int64","name":"ReadOperationCount","offset":928,"parentId":"2000"},
|
||||||
|
{"id":"2070","kind":"Int64","name":"WriteOperationCount","offset":936,"parentId":"2000"},
|
||||||
|
{"id":"2071","kind":"Int64","name":"OtherOperationCount","offset":944,"parentId":"2000"},
|
||||||
|
{"id":"2072","kind":"Int64","name":"ReadTransferCount","offset":952,"parentId":"2000"},
|
||||||
|
{"id":"2073","kind":"Int64","name":"WriteTransferCount","offset":960,"parentId":"2000"},
|
||||||
|
{"id":"2074","kind":"Int64","name":"OtherTransferCount","offset":968,"parentId":"2000"},
|
||||||
|
{"id":"2075","kind":"UInt64","name":"CommitChargeLimit","offset":976,"parentId":"2000"},
|
||||||
|
{"id":"2076","kind":"UInt64","name":"CommitCharge","offset":984,"parentId":"2000"},
|
||||||
|
{"id":"2077","kind":"UInt64","name":"CommitChargePeak","offset":992,"parentId":"2000"},
|
||||||
|
{"id":"2078","kind":"Struct","name":"Vm","structTypeName":"_MMSUPPORT_FULL","offset":1024,"parentId":"2000","refId":"350","collapsed":true},
|
||||||
|
{"id":"2079","kind":"Struct","name":"MmProcessLinks","structTypeName":"_LIST_ENTRY","offset":1344,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2080","kind":"UInt32","name":"ModifiedPageCount","offset":1360,"parentId":"2000"},
|
||||||
|
{"id":"2081","kind":"Int32","name":"ExitStatus","offset":1364,"parentId":"2000"},
|
||||||
|
{"id":"2082","kind":"Struct","name":"VadRoot","structTypeName":"_RTL_AVL_TREE","offset":1368,"parentId":"2000","refId":"170","collapsed":true},
|
||||||
|
{"id":"2083","kind":"Pointer64","name":"VadHint","offset":1376,"parentId":"2000"},
|
||||||
|
{"id":"2084","kind":"UInt64","name":"VadCount","offset":1384,"parentId":"2000"},
|
||||||
|
{"id":"2085","kind":"UInt64","name":"VadPhysicalPages","offset":1392,"parentId":"2000"},
|
||||||
|
{"id":"2086","kind":"UInt64","name":"VadPhysicalPagesLimit","offset":1400,"parentId":"2000"},
|
||||||
|
{"id":"2087","kind":"Struct","name":"AlpcContext","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":1408,"parentId":"2000","refId":"250","collapsed":true},
|
||||||
|
{"id":"2088","kind":"Struct","name":"TimerResolutionLink","structTypeName":"_LIST_ENTRY","offset":1440,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2089","kind":"Pointer64","name":"TimerResolutionStackRecord","offset":1456,"parentId":"2000"},
|
||||||
|
{"id":"2090","kind":"UInt32","name":"RequestedTimerResolution","offset":1464,"parentId":"2000"},
|
||||||
|
{"id":"2091","kind":"UInt32","name":"SmallestTimerResolution","offset":1468,"parentId":"2000"},
|
||||||
|
{"id":"2092","kind":"Int64","name":"ExitTime","offset":1472,"parentId":"2000"},
|
||||||
|
{"id":"2093","kind":"Pointer64","name":"InvertedFunctionTable","offset":1480,"parentId":"2000"},
|
||||||
|
{"id":"2094","kind":"Struct","name":"InvertedFunctionTableLock","structTypeName":"_EX_PUSH_LOCK","offset":1488,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2095","kind":"UInt32","name":"ActiveThreadsHighWatermark","offset":1496,"parentId":"2000"},
|
||||||
|
{"id":"2096","kind":"UInt32","name":"LargePrivateVadCount","offset":1500,"parentId":"2000"},
|
||||||
|
{"id":"2097","kind":"Struct","name":"ThreadListLock","structTypeName":"_EX_PUSH_LOCK","offset":1504,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2098","kind":"Pointer64","name":"WnfContext","offset":1512,"parentId":"2000"},
|
||||||
|
{"id":"2099","kind":"Pointer64","name":"ServerSilo","offset":1520,"parentId":"2000"},
|
||||||
|
{"id":"2100","kind":"UInt8","name":"SignatureLevel","offset":1528,"parentId":"2000"},
|
||||||
|
{"id":"2101","kind":"UInt8","name":"SectionSignatureLevel","offset":1529,"parentId":"2000"},
|
||||||
|
{"id":"2102","kind":"Struct","name":"Protection","structTypeName":"_PS_PROTECTION","offset":1530,"parentId":"2000","refId":"210","collapsed":true},
|
||||||
|
{"id":"2103","kind":"Hex8","name":"HangCount:3 GhostCount:3 PrefilterException:1","offset":1531,"parentId":"2000"},
|
||||||
|
{"id":"2104","kind":"Struct","name":"","classKeyword":"union","offset":1532,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2105","kind":"UInt32","name":"Flags3","offset":0,"parentId":"2104"},
|
||||||
|
{"id":"2106","kind":"Hex32","name":"Minimal:1 ReplacingPageRoot:1 Crashed:1 JobVadsAreTracked:1 VadTrackingDisabled:1 AuxiliaryProcess:1 SubsystemProcess:1 IndirectCpuSets:1 RelinquishedCommit:1 HighGraphicsPriority:1 CommitFailLogged:1 ReserveFailLogged:1 SystemProcess:1","offset":0,"parentId":"2104"},
|
||||||
|
{"id":"2107","kind":"Int32","name":"DeviceAsid","offset":1536,"parentId":"2000"},
|
||||||
|
{"id":"2108","kind":"Pointer64","name":"SvmData","offset":1544,"parentId":"2000"},
|
||||||
|
{"id":"2109","kind":"Struct","name":"SvmProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":1552,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2110","kind":"UInt64","name":"SvmLock","offset":1560,"parentId":"2000"},
|
||||||
|
{"id":"2111","kind":"Struct","name":"SvmProcessDeviceListHead","structTypeName":"_LIST_ENTRY","offset":1568,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2112","kind":"UInt64","name":"LastFreezeInterruptTime","offset":1584,"parentId":"2000"},
|
||||||
|
{"id":"2113","kind":"Pointer64","name":"DiskCounters","offset":1592,"parentId":"2000"},
|
||||||
|
{"id":"2114","kind":"Pointer64","name":"PicoContext","offset":1600,"parentId":"2000"},
|
||||||
|
{"id":"2115","kind":"Pointer64","name":"EnclaveTable","offset":1608,"parentId":"2000"},
|
||||||
|
{"id":"2116","kind":"UInt64","name":"EnclaveNumber","offset":1616,"parentId":"2000"},
|
||||||
|
{"id":"2117","kind":"Struct","name":"EnclaveLock","structTypeName":"_EX_PUSH_LOCK","offset":1624,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2118","kind":"UInt32","name":"HighPriorityFaultsAllowed","offset":1632,"parentId":"2000"},
|
||||||
|
{"id":"2119","kind":"Pointer64","name":"EnergyContext","offset":1640,"parentId":"2000"},
|
||||||
|
{"id":"2120","kind":"Pointer64","name":"VmContext","offset":1648,"parentId":"2000"},
|
||||||
|
{"id":"2121","kind":"UInt64","name":"SequenceNumber","offset":1656,"parentId":"2000"},
|
||||||
|
{"id":"2122","kind":"UInt64","name":"CreateInterruptTime","offset":1664,"parentId":"2000"},
|
||||||
|
{"id":"2123","kind":"UInt64","name":"CreateUnbiasedInterruptTime","offset":1672,"parentId":"2000"},
|
||||||
|
{"id":"2124","kind":"UInt64","name":"TotalUnbiasedFrozenTime","offset":1680,"parentId":"2000"},
|
||||||
|
{"id":"2125","kind":"UInt64","name":"LastAppStateUpdateTime","offset":1688,"parentId":"2000"},
|
||||||
|
{"id":"2126","kind":"Hex64","name":"LastAppStateUptime:61 LastAppState:3","offset":1696,"parentId":"2000"},
|
||||||
|
{"id":"2127","kind":"UInt64","name":"SharedCommitCharge","offset":1704,"parentId":"2000"},
|
||||||
|
{"id":"2128","kind":"Struct","name":"SharedCommitLock","structTypeName":"_EX_PUSH_LOCK","offset":1712,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2129","kind":"Struct","name":"SharedCommitLinks","structTypeName":"_LIST_ENTRY","offset":1720,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2130","kind":"UInt64","name":"AllowedCpuSets","offset":1736,"parentId":"2000"},
|
||||||
|
{"id":"2131","kind":"UInt64","name":"DefaultCpuSets","offset":1744,"parentId":"2000"},
|
||||||
|
{"id":"2132","kind":"Pointer64","name":"DiskIoAttribution","offset":1752,"parentId":"2000"},
|
||||||
|
{"id":"2133","kind":"Pointer64","name":"DxgProcess","offset":1760,"parentId":"2000"},
|
||||||
|
{"id":"2134","kind":"UInt32","name":"Win32KFilterSet","offset":1768,"parentId":"2000"},
|
||||||
|
{"id":"2135","kind":"UInt16","name":"Machine","offset":1772,"parentId":"2000"},
|
||||||
|
{"id":"2136","kind":"UInt8","name":"MmSlabIdentity","offset":1774,"parentId":"2000"},
|
||||||
|
{"id":"2137","kind":"UInt8","name":"Spare0","offset":1775,"parentId":"2000"},
|
||||||
|
{"id":"2138","kind":"Struct","name":"ProcessTimerDelay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","offset":1776,"parentId":"2000","refId":"220","collapsed":true},
|
||||||
|
{"id":"2139","kind":"UInt32","name":"KTimerSets","offset":1784,"parentId":"2000"},
|
||||||
|
{"id":"2140","kind":"UInt32","name":"KTimer2Sets","offset":1788,"parentId":"2000"},
|
||||||
|
{"id":"2141","kind":"UInt32","name":"ThreadTimerSets","offset":1792,"parentId":"2000"},
|
||||||
|
{"id":"2142","kind":"UInt64","name":"VirtualTimerListLock","offset":1800,"parentId":"2000"},
|
||||||
|
{"id":"2143","kind":"Struct","name":"VirtualTimerListHead","structTypeName":"_LIST_ENTRY","offset":1808,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2144","kind":"Struct","name":"WakeChannel","structTypeName":"_WNF_STATE_NAME","offset":1824,"parentId":"2000","refId":"230","collapsed":true},
|
||||||
|
{"id":"2145","kind":"Struct","name":"","classKeyword":"union","offset":1872,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2146","kind":"UInt32","name":"MitigationFlags","offset":0,"parentId":"2145"},
|
||||||
|
{"id":"2147","kind":"Hex32","name":"ControlFlowGuardEnabled:1 ControlFlowGuardExportSuppressionEnabled:1 ControlFlowGuardStrict:1 DisallowStrippedImages:1 ForceRelocateImages:1 HighEntropyASLREnabled:1 StackRandomizationDisabled:1 ExtensionPointDisable:1 DisableDynamicCode:1","offset":0,"parentId":"2145"},
|
||||||
|
{"id":"2148","kind":"Struct","name":"","classKeyword":"union","offset":1876,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2149","kind":"UInt32","name":"MitigationFlags2","offset":0,"parentId":"2148"},
|
||||||
|
{"id":"2150","kind":"Hex32","name":"EnableExportAddressFilter:1 AuditExportAddressFilter:1 EnableRopStackPivot:1 AuditRopStackPivot:1 CetUserShadowStacks:1 SpeculativeStoreBypassDisable:1","offset":0,"parentId":"2148"},
|
||||||
|
{"id":"2151","kind":"Pointer64","name":"PartitionObject","offset":1880,"parentId":"2000"},
|
||||||
|
{"id":"2152","kind":"UInt64","name":"SecurityDomain","offset":1888,"parentId":"2000"},
|
||||||
|
{"id":"2153","kind":"UInt64","name":"ParentSecurityDomain","offset":1896,"parentId":"2000"},
|
||||||
|
{"id":"2154","kind":"Pointer64","name":"CoverageSamplerContext","offset":1904,"parentId":"2000"},
|
||||||
|
{"id":"2155","kind":"Pointer64","name":"MmHotPatchContext","offset":1912,"parentId":"2000"},
|
||||||
|
{"id":"2156","kind":"Struct","name":"DynamicEHContinuationTargetsTree","structTypeName":"_RTL_AVL_TREE","offset":1920,"parentId":"2000","refId":"170","collapsed":true},
|
||||||
|
{"id":"2157","kind":"Struct","name":"DynamicEHContinuationTargetsLock","structTypeName":"_EX_PUSH_LOCK","offset":1928,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2158","kind":"Struct","name":"DynamicEnforcedCetCompatibleRanges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":1936,"parentId":"2000","refId":"240","collapsed":true},
|
||||||
|
{"id":"2159","kind":"UInt32","name":"DisabledComponentFlags","offset":1952,"parentId":"2000"},
|
||||||
|
{"id":"2160","kind":"Int32","name":"PageCombineSequence","offset":1956,"parentId":"2000"},
|
||||||
|
{"id":"2161","kind":"Struct","name":"EnableOptionalXStateFeaturesLock","structTypeName":"_EX_PUSH_LOCK","offset":1960,"parentId":"2000","refId":"120","collapsed":true},
|
||||||
|
{"id":"2162","kind":"Pointer64","name":"PathRedirectionHashes","offset":1968,"parentId":"2000"},
|
||||||
|
{"id":"2163","kind":"Pointer64","name":"SyscallProvider","offset":1976,"parentId":"2000"},
|
||||||
|
{"id":"2164","kind":"Struct","name":"SyscallProviderProcessLinks","structTypeName":"_LIST_ENTRY","offset":1984,"parentId":"2000","refId":"100","collapsed":true},
|
||||||
|
{"id":"2165","kind":"Hex64","name":"SyscallProviderDispatchContext","offset":2000,"parentId":"2000"},
|
||||||
|
{"id":"2166","kind":"Struct","name":"","classKeyword":"union","offset":2008,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2167","kind":"UInt32","name":"MitigationFlags3","offset":0,"parentId":"2166"},
|
||||||
|
{"id":"2168","kind":"Hex32","name":"RestrictCoreSharing:1 DisallowFsctlSystemCalls:1 AuditDisallowFsctlSystemCalls:1 MitigationFlags3Spare:29","offset":0,"parentId":"2166"},
|
||||||
|
{"id":"2169","kind":"Struct","name":"","classKeyword":"union","offset":2012,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2170","kind":"UInt32","name":"Flags4","offset":0,"parentId":"2169"},
|
||||||
|
{"id":"2171","kind":"Hex32","name":"ThreadWasActive:1 MinimalTerminate:1 ImageExpansionDisable:1 SessionFirstProcess:1","offset":0,"parentId":"2169"},
|
||||||
|
{"id":"2172","kind":"Struct","name":"","classKeyword":"union","offset":2016,"parentId":"2000","refId":"0","collapsed":true},
|
||||||
|
{"id":"2173","kind":"UInt32","name":"SyscallUsage","offset":0,"parentId":"2172"},
|
||||||
|
{"id":"2174","kind":"Hex32","name":"SystemModuleInformation:1 SystemModuleInformationEx:1 SystemLocksInformation:1 SystemHandleInformation:1 SystemExtendedHandleInformation:1","offset":0,"parentId":"2172"},
|
||||||
|
{"id":"2175","kind":"Int32","name":"SupervisorDeviceAsid","offset":2020,"parentId":"2000"},
|
||||||
|
{"id":"2176","kind":"Pointer64","name":"SupervisorSvmData","offset":2024,"parentId":"2000"},
|
||||||
|
{"id":"2177","kind":"Pointer64","name":"NetworkCounters","offset":2032,"parentId":"2000"},
|
||||||
|
{"id":"2178","kind":"Hex64","name":"Execution","offset":2040,"parentId":"2000"},
|
||||||
|
{"id":"2179","kind":"Pointer64","name":"ThreadIndexTable","offset":2048,"parentId":"2000"},
|
||||||
|
{"id":"2180","kind":"Struct","name":"FreezeWorkLinks","structTypeName":"_LIST_ENTRY","offset":2056,"parentId":"2000","refId":"100","collapsed":true}
|
||||||
|
]
|
||||||
|
}
|
||||||
616
src/examples/MMPFN.rcx
Normal file
616
src/examples/MMPFN.rcx
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
{
|
||||||
|
"baseAddress": "FFFFCA8010000000",
|
||||||
|
"nextId": "3000",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "100",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "list_entry",
|
||||||
|
"structTypeName": "_LIST_ENTRY",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "101",
|
||||||
|
"kind": "Pointer64",
|
||||||
|
"name": "Flink",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "100",
|
||||||
|
"refId": "100",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "102",
|
||||||
|
"kind": "Pointer64",
|
||||||
|
"name": "Blink",
|
||||||
|
"offset": 8,
|
||||||
|
"parentId": "100",
|
||||||
|
"refId": "100",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "200",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "balanced_node",
|
||||||
|
"structTypeName": "_RTL_BALANCED_NODE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "210",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "200",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "211",
|
||||||
|
"kind": "Pointer64",
|
||||||
|
"name": "Left",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "210",
|
||||||
|
"refId": "200",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "212",
|
||||||
|
"kind": "Pointer64",
|
||||||
|
"name": "Right",
|
||||||
|
"offset": 8,
|
||||||
|
"parentId": "210",
|
||||||
|
"refId": "200",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "220",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 16,
|
||||||
|
"parentId": "200",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "221",
|
||||||
|
"kind": "UInt64",
|
||||||
|
"name": "ParentValue",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "220"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "300",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte",
|
||||||
|
"structTypeName": "_MMPTE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "301",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "300",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "302",
|
||||||
|
"kind": "UInt64",
|
||||||
|
"name": "Long",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "303",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "Hard",
|
||||||
|
"structTypeName": "_MMPTE_HARDWARE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "400",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "304",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "Proto",
|
||||||
|
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "600",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "305",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "Soft",
|
||||||
|
"structTypeName": "_MMPTE_SOFTWARE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "500",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "306",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "Trans",
|
||||||
|
"structTypeName": "_MMPTE_TRANSITION",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "700",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "307",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "Subsect",
|
||||||
|
"structTypeName": "_MMPTE_SUBSECTION",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "800",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "308",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "TimeStamp",
|
||||||
|
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "900",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "309",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "List",
|
||||||
|
"structTypeName": "_MMPTE_LIST",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "301",
|
||||||
|
"refId": "1000",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "400",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_hardware",
|
||||||
|
"structTypeName": "_MMPTE_HARDWARE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "401",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "400"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "500",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_software",
|
||||||
|
"structTypeName": "_MMPTE_SOFTWARE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "501",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "500"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "600",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_prototype",
|
||||||
|
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "601",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "600"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "700",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_transition",
|
||||||
|
"structTypeName": "_MMPTE_TRANSITION",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "701",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "700"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "800",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_subsection",
|
||||||
|
"structTypeName": "_MMPTE_SUBSECTION",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "801",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "800"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "900",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_timestamp",
|
||||||
|
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "901",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "900"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1000",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpte_list",
|
||||||
|
"structTypeName": "_MMPTE_LIST",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1001",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1000"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1100",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mipfnflink",
|
||||||
|
"structTypeName": "_MIPFNFLINK",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1101",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Flink",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1100"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1200",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mipfnblink",
|
||||||
|
"structTypeName": "_MIPFNBLINK",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1201",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Blink",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1200"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1300",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpfnentry1",
|
||||||
|
"structTypeName": "_MMPFNENTRY1",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1301",
|
||||||
|
"kind": "Hex8",
|
||||||
|
"name": "Flags",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1300"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1400",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpfnentry3",
|
||||||
|
"structTypeName": "_MMPFNENTRY3",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1401",
|
||||||
|
"kind": "Hex8",
|
||||||
|
"name": "Flags",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1400"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1500",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mi_pfn_flags",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1501",
|
||||||
|
"kind": "Hex32",
|
||||||
|
"name": "Flags",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1500"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1600",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mi_pfn_flags4",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS4",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1601",
|
||||||
|
"kind": "Hex64",
|
||||||
|
"name": "Flags",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1600"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "1700",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mi_pfn_flags5",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS5",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1701",
|
||||||
|
"kind": "Hex32",
|
||||||
|
"name": "Flags",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "1700"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "2000",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "mmpfn",
|
||||||
|
"structTypeName": "_MMPFN",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "0",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2001",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2000",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2010",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "ListEntry",
|
||||||
|
"structTypeName": "_LIST_ENTRY",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2001",
|
||||||
|
"refId": "100",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2011",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "TreeNode",
|
||||||
|
"structTypeName": "_RTL_BALANCED_NODE",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2001",
|
||||||
|
"refId": "200",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2012",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2001",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2013",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u1",
|
||||||
|
"structTypeName": "_MIPFNFLINK",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2012",
|
||||||
|
"refId": "1100",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2014",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 8,
|
||||||
|
"parentId": "2012",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2015",
|
||||||
|
"kind": "Pointer64",
|
||||||
|
"name": "PteAddress",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2014",
|
||||||
|
"refId": "300",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2016",
|
||||||
|
"kind": "UInt64",
|
||||||
|
"name": "PteLong",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2017",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "OriginalPte",
|
||||||
|
"structTypeName": "_MMPTE",
|
||||||
|
"offset": 16,
|
||||||
|
"parentId": "2012",
|
||||||
|
"refId": "300",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "2020",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u2",
|
||||||
|
"structTypeName": "_MIPFNBLINK",
|
||||||
|
"offset": 24,
|
||||||
|
"parentId": "2000",
|
||||||
|
"refId": "1200",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "2030",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u3",
|
||||||
|
"classKeyword": "union",
|
||||||
|
"offset": 32,
|
||||||
|
"parentId": "2000",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2031",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2030",
|
||||||
|
"refId": "0",
|
||||||
|
"collapsed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2032",
|
||||||
|
"kind": "UInt16",
|
||||||
|
"name": "ReferenceCount",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2031"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2033",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "e1",
|
||||||
|
"structTypeName": "_MMPFNENTRY1",
|
||||||
|
"offset": 2,
|
||||||
|
"parentId": "2031",
|
||||||
|
"refId": "1300",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2034",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "e4",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS",
|
||||||
|
"offset": 0,
|
||||||
|
"parentId": "2030",
|
||||||
|
"refId": "1500",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "2040",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u5",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS5",
|
||||||
|
"offset": 36,
|
||||||
|
"parentId": "2000",
|
||||||
|
"refId": "1700",
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "2050",
|
||||||
|
"kind": "Struct",
|
||||||
|
"name": "u4",
|
||||||
|
"structTypeName": "_MI_PFN_FLAGS4",
|
||||||
|
"offset": 40,
|
||||||
|
"parentId": "2000",
|
||||||
|
"refId": "1600",
|
||||||
|
"collapsed": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
100
src/format.cpp
100
src/format.cpp
@@ -23,6 +23,14 @@ static QString fit(QString s, int w) {
|
|||||||
return s.leftJustified(w, ' ');
|
return s.leftJustified(w, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
|
||||||
|
static QString fitOverflow(const QString& s, int w) {
|
||||||
|
if (w <= 0) return {};
|
||||||
|
if (s.size() <= w)
|
||||||
|
return s.leftJustified(w, ' ');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Type name ──
|
// ── Type name ──
|
||||||
|
|
||||||
// Override seam: injectable type-name provider
|
// Override seam: injectable type-name provider
|
||||||
@@ -113,15 +121,8 @@ QString fmtDouble(double v) {
|
|||||||
}
|
}
|
||||||
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
||||||
|
|
||||||
QString fmtPointer32(uint32_t v) {
|
QString fmtPointer32(uint32_t v) { return hexVal(v); }
|
||||||
if (v == 0) return QStringLiteral("-> NULL");
|
QString fmtPointer64(uint64_t v) { return hexVal(v); }
|
||||||
return QStringLiteral("-> ") + hexVal(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString fmtPointer64(uint64_t v) {
|
|
||||||
if (v == 0) return QStringLiteral("-> NULL");
|
|
||||||
return QStringLiteral("-> ") + hexVal(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Indentation ──
|
// ── Indentation ──
|
||||||
|
|
||||||
@@ -140,20 +141,25 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
|
|||||||
// ── Struct type name (for width calculation) ──
|
// ── Struct type name (for width calculation) ──
|
||||||
|
|
||||||
QString structTypeName(const Node& node) {
|
QString structTypeName(const Node& node) {
|
||||||
// Full type string: "struct TypeName" or just "struct" if no typename
|
// Named types: just the type name (e.g. "_LIST_ENTRY")
|
||||||
QString base = typeName(node.kind).trimmed(); // "struct"
|
// Anonymous: just the keyword (e.g. "union", "struct")
|
||||||
if (!node.structTypeName.isEmpty())
|
if (!node.structTypeName.isEmpty())
|
||||||
return base + QStringLiteral(" ") + node.structTypeName;
|
return node.structTypeName;
|
||||||
return base;
|
return node.resolvedClassKeyword();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Struct header / footer ──
|
// ── Struct header / footer ──
|
||||||
|
|
||||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
|
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
|
||||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(structTypeName(node), colType);
|
QString rawType = structTypeName(node);
|
||||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||||
|
if (node.name.isEmpty()) {
|
||||||
|
// Anonymous struct/union: "union {" with no column padding
|
||||||
|
return ind + rawType + SEP + suffix;
|
||||||
|
}
|
||||||
|
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||||
return ind + type + SEP + node.name + SEP + suffix;
|
return ind + type + SEP + node.name + SEP + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +169,10 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
|||||||
|
|
||||||
// ── Array header ──
|
// ── Array header ──
|
||||||
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
||||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) {
|
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType);
|
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||||
|
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||||
return ind + type + SEP + node.name + SEP + suffix;
|
return ind + type + SEP + node.name + SEP + suffix;
|
||||||
}
|
}
|
||||||
@@ -174,10 +181,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
|
|||||||
|
|
||||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||||
const Provider& prov, uint64_t addr,
|
const Provider& prov, uint64_t addr,
|
||||||
const QString& ptrTypeName, int colType, int colName) {
|
const QString& ptrTypeName, int colType, int colName,
|
||||||
|
bool compact) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = fit(ptrTypeName, colType);
|
bool overflow = compact && ptrTypeName.size() > colType;
|
||||||
|
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
|
if (overflow) {
|
||||||
|
// Overflow: no column padding
|
||||||
|
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
|
||||||
|
}
|
||||||
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||||
QString name = fit(node.name, colName);
|
QString name = fit(node.name, colName);
|
||||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||||
@@ -366,12 +379,22 @@ QString readValue(const Node& node, const Provider& prov,
|
|||||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||||
uint64_t addr, int depth, int subLine,
|
uint64_t addr, int depth, int subLine,
|
||||||
const QString& comment, int colType, int colName,
|
const QString& comment, int colType, int colName,
|
||||||
const QString& typeOverride) {
|
const QString& typeOverride, bool compact) {
|
||||||
QString ind = indent(depth);
|
QString ind = indent(depth);
|
||||||
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
|
|
||||||
QString name = fit(node.name, colName);
|
// Compute raw type string for overflow detection
|
||||||
|
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
|
||||||
|
bool overflow = compact && rawType.size() > colType;
|
||||||
|
|
||||||
|
QString type = overflow ? fitOverflow(rawType, colType)
|
||||||
|
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
|
||||||
|
: fit(typeOverride, colType));
|
||||||
|
QString name = overflow ? node.name : fit(node.name, colName);
|
||||||
|
|
||||||
|
// Effective column width for this line (accounts for overflow, clamped to hard max)
|
||||||
|
int effectiveColType = overflow ? rawType.size() : colType;
|
||||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||||
const int prefixW = colType + colName + 2 * kSepWidth;
|
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
|
||||||
|
|
||||||
// Comment suffix (only present when a comment is provided; no trailing padding)
|
// Comment suffix (only present when a comment is provided; no trailing padding)
|
||||||
QString cmtSuffix = comment.isEmpty() ? QString()
|
QString cmtSuffix = comment.isEmpty() ? QString()
|
||||||
@@ -394,7 +417,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
|||||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
QString val = overflow ? readValue(node, prov, addr, subLine)
|
||||||
|
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||||
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,4 +698,32 @@ QString validateBaseAddress(const QString& text) {
|
|||||||
return AddressParser::validate(s);
|
return AddressParser::validate(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
|
||||||
|
QString ind = indent(depth);
|
||||||
|
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
|
} // namespace rcx::fmt
|
||||||
|
|||||||
@@ -315,7 +315,8 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
|||||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
||||||
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
|
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
|
||||||
|
fwdKw = QStringLiteral("struct");
|
||||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||||
ctx.forwardDeclared.insert(child.refId);
|
ctx.forwardDeclared.insert(child.refId);
|
||||||
}
|
}
|
||||||
@@ -327,7 +328,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
|||||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||||
|
|
||||||
QString kw = node.resolvedClassKeyword();
|
QString kw = node.resolvedClassKeyword();
|
||||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
|
||||||
|
// Enum with members: emit as proper C enum
|
||||||
|
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||||
|
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
|
||||||
|
for (const auto& m : node.enumMembers) {
|
||||||
|
ctx.output += QStringLiteral(" %1 = %2,\n")
|
||||||
|
.arg(sanitizeIdent(m.first))
|
||||||
|
.arg(m.second);
|
||||||
|
}
|
||||||
|
ctx.output += QStringLiteral("};\n\n");
|
||||||
|
ctx.visiting.remove(structId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
|
||||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||||
|
|
||||||
emitStructBody(ctx, structId);
|
emitStructBody(ctx, structId);
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* er
|
|||||||
while (i < children.size()) {
|
while (i < children.size()) {
|
||||||
const Node& child = tree.nodes[children[i]];
|
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)
|
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||||
if (isHexNode(child.kind)) {
|
if (isHexNode(child.kind)) {
|
||||||
int runStart = child.offset;
|
int runStart = child.offset;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QPair>
|
#include <QPair>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
// ── RawPDB headers ──
|
// ── RawPDB headers ──
|
||||||
#include "PDB.h"
|
#include "PDB.h"
|
||||||
@@ -232,10 +233,16 @@ struct PdbCtx {
|
|||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
const TypeTable* tt = nullptr;
|
const TypeTable* tt = nullptr;
|
||||||
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
|
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
|
||||||
|
QHash<QString, uint32_t> structDefByName; // struct/class definition name → typeIndex
|
||||||
|
QHash<QString, uint32_t> unionDefByName; // union definition name → typeIndex
|
||||||
|
bool udtDefIndexBuilt = false;
|
||||||
|
|
||||||
uint64_t importUDT(uint32_t typeIndex);
|
uint64_t importUDT(uint32_t typeIndex);
|
||||||
|
uint64_t importEnum(uint32_t typeIndex);
|
||||||
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
|
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
|
||||||
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
|
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
|
||||||
|
void buildUdtDefinitionIndex();
|
||||||
|
uint32_t findUdtDefinitionIndex(TRK kind, const char* typeName);
|
||||||
|
|
||||||
// Resolve LF_MODIFIER chain to underlying type index
|
// Resolve LF_MODIFIER chain to underlying type index
|
||||||
uint32_t unwrapModifier(uint32_t typeIndex) const {
|
uint32_t unwrapModifier(uint32_t typeIndex) const {
|
||||||
@@ -248,6 +255,56 @@ struct PdbCtx {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void PdbCtx::buildUdtDefinitionIndex() {
|
||||||
|
if (udtDefIndexBuilt || !tt) return;
|
||||||
|
udtDefIndexBuilt = true;
|
||||||
|
|
||||||
|
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
||||||
|
const auto* rec = tt->get(ti);
|
||||||
|
if (!rec) continue;
|
||||||
|
|
||||||
|
bool isUnion = false;
|
||||||
|
bool isFwd = false;
|
||||||
|
const char* candidateName = nullptr;
|
||||||
|
|
||||||
|
if (rec->header.kind == TRK::LF_UNION) {
|
||||||
|
isUnion = true;
|
||||||
|
isFwd = rec->data.LF_UNION.property.fwdref;
|
||||||
|
candidateName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
|
||||||
|
} else if (rec->header.kind == TRK::LF_STRUCTURE || rec->header.kind == TRK::LF_CLASS) {
|
||||||
|
isFwd = rec->data.LF_CLASS.property.fwdref;
|
||||||
|
candidateName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFwd || !candidateName || candidateName[0] == '\0') continue;
|
||||||
|
|
||||||
|
QString qname = QString::fromUtf8(candidateName);
|
||||||
|
QHash<QString, uint32_t>& lookup = isUnion ? unionDefByName : structDefByName;
|
||||||
|
if (!lookup.contains(qname)) lookup.insert(qname, ti);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t PdbCtx::findUdtDefinitionIndex(TRK kind, const char* typeName) {
|
||||||
|
if (!typeName || typeName[0] == '\0') return 0;
|
||||||
|
|
||||||
|
buildUdtDefinitionIndex();
|
||||||
|
|
||||||
|
const QString qname = QString::fromUtf8(typeName);
|
||||||
|
if (kind == TRK::LF_UNION) {
|
||||||
|
auto it = unionDefByName.constFind(qname);
|
||||||
|
return (it != unionDefByName.cend()) ? it.value() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == TRK::LF_STRUCTURE || kind == TRK::LF_CLASS) {
|
||||||
|
auto it = structDefByName.constFind(qname);
|
||||||
|
return (it != structDefByName.cend()) ? it.value() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
|
uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
|
||||||
if (typeIndex < tt->firstIndex()) return 0;
|
if (typeIndex < tt->firstIndex()) return 0;
|
||||||
|
|
||||||
@@ -300,12 +357,66 @@ uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
|
|||||||
return nodeId;
|
return nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint64_t PdbCtx::importEnum(uint32_t typeIndex) {
|
||||||
|
if (typeIndex < tt->firstIndex()) return 0;
|
||||||
|
|
||||||
|
auto it = typeCache.find(typeIndex);
|
||||||
|
if (it != typeCache.end()) return it.value();
|
||||||
|
|
||||||
|
const auto* rec = tt->get(typeIndex);
|
||||||
|
if (!rec || rec->header.kind != TRK::LF_ENUM) return 0;
|
||||||
|
if (rec->data.LF_ENUM.property.fwdref) return 0;
|
||||||
|
|
||||||
|
QString qname = rec->data.LF_ENUM.name
|
||||||
|
? QString::fromUtf8(rec->data.LF_ENUM.name)
|
||||||
|
: QStringLiteral("<anon>");
|
||||||
|
|
||||||
|
Node s;
|
||||||
|
s.kind = NodeKind::Struct;
|
||||||
|
s.name = qname;
|
||||||
|
s.structTypeName = qname;
|
||||||
|
s.classKeyword = QStringLiteral("enum");
|
||||||
|
s.parentId = 0;
|
||||||
|
s.collapsed = true;
|
||||||
|
|
||||||
|
// Extract enum members from field list
|
||||||
|
uint32_t fieldListIndex = rec->data.LF_ENUM.field;
|
||||||
|
const auto* flRec = tt->get(fieldListIndex);
|
||||||
|
if (flRec && flRec->header.kind == TRK::LF_FIELDLIST) {
|
||||||
|
auto maxSize = flRec->header.size - sizeof(uint16_t);
|
||||||
|
for (size_t i = 0; i < maxSize; ) {
|
||||||
|
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
||||||
|
reinterpret_cast<const uint8_t*>(&flRec->data.LF_FIELD.list) + i);
|
||||||
|
if (field->kind != TRK::LF_ENUMERATE) break;
|
||||||
|
|
||||||
|
int64_t val = static_cast<int64_t>(leafValue(
|
||||||
|
field->data.LF_ENUMERATE.value,
|
||||||
|
field->data.LF_ENUMERATE.lfEasy.kind));
|
||||||
|
const char* eName = leafName(
|
||||||
|
field->data.LF_ENUMERATE.value,
|
||||||
|
field->data.LF_ENUMERATE.lfEasy.kind);
|
||||||
|
if (eName)
|
||||||
|
s.enumMembers.append({QString::fromUtf8(eName), val});
|
||||||
|
|
||||||
|
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
|
||||||
|
i += strnlen(eName, maxSize - i - 1) + 1;
|
||||||
|
i = (i + 3) & ~size_t(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = tree.addNode(s);
|
||||||
|
uint64_t nodeId = tree.nodes[idx].id;
|
||||||
|
typeCache[typeIndex] = nodeId;
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
||||||
const auto* rec = tt->get(fieldListIndex);
|
const auto* rec = tt->get(fieldListIndex);
|
||||||
if (!rec || rec->header.kind != TRK::LF_FIELDLIST) return;
|
if (!rec || rec->header.kind != TRK::LF_FIELDLIST) return;
|
||||||
|
|
||||||
auto maximumSize = rec->header.size - sizeof(uint16_t);
|
auto maximumSize = rec->header.size - sizeof(uint16_t);
|
||||||
QSet<QPair<int,int>> bitfieldSlots;
|
QSet<QPair<int,int>> bitfieldSlots;
|
||||||
|
QHash<QPair<int,int>, uint64_t> bitfieldNodeIds;
|
||||||
|
|
||||||
for (size_t i = 0; i < maximumSize; ) {
|
for (size_t i = 0; i < maximumSize; ) {
|
||||||
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
||||||
@@ -331,7 +442,7 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
|||||||
if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) {
|
if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) {
|
||||||
uint32_t underlying = typeRec->data.LF_BITFIELD.type;
|
uint32_t underlying = typeRec->data.LF_BITFIELD.type;
|
||||||
uint8_t bitLen = typeRec->data.LF_BITFIELD.length;
|
uint8_t bitLen = typeRec->data.LF_BITFIELD.length;
|
||||||
(void)bitLen;
|
uint8_t bitPos = typeRec->data.LF_BITFIELD.position;
|
||||||
|
|
||||||
// Determine slot size from underlying type
|
// Determine slot size from underlying type
|
||||||
uint64_t slotSize = 4;
|
uint64_t slotSize = 4;
|
||||||
@@ -343,12 +454,26 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
|||||||
auto key = qMakePair((int)offset, (int)slotSize);
|
auto key = qMakePair((int)offset, (int)slotSize);
|
||||||
if (!bitfieldSlots.contains(key)) {
|
if (!bitfieldSlots.contains(key)) {
|
||||||
bitfieldSlots.insert(key);
|
bitfieldSlots.insert(key);
|
||||||
|
// Create bitfield container node
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = hexForSize(slotSize);
|
n.kind = NodeKind::Struct;
|
||||||
n.name = qname;
|
n.classKeyword = QStringLiteral("bitfield");
|
||||||
|
n.elementKind = hexForSize(slotSize);
|
||||||
n.parentId = parentId;
|
n.parentId = parentId;
|
||||||
n.offset = offset;
|
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 {
|
} else {
|
||||||
importMemberType(memberType, offset, qname, parentId);
|
importMemberType(memberType, offset, qname, parentId);
|
||||||
@@ -522,7 +647,6 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
isFwd = pointeeRec->data.LF_CLASS.property.fwdref;
|
isFwd = pointeeRec->data.LF_CLASS.property.fwdref;
|
||||||
|
|
||||||
if (isFwd) {
|
if (isFwd) {
|
||||||
// Need to find the non-fwdref definition by name
|
|
||||||
const char* typeName = nullptr;
|
const char* typeName = nullptr;
|
||||||
if (pointeeRec->header.kind == TRK::LF_UNION)
|
if (pointeeRec->header.kind == TRK::LF_UNION)
|
||||||
typeName = leafName(pointeeRec->data.LF_UNION.data, unionLeafKind(pointeeRec->data.LF_UNION.data));
|
typeName = leafName(pointeeRec->data.LF_UNION.data, unionLeafKind(pointeeRec->data.LF_UNION.data));
|
||||||
@@ -530,30 +654,24 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
typeName = leafName(pointeeRec->data.LF_CLASS.data,
|
typeName = leafName(pointeeRec->data.LF_CLASS.data,
|
||||||
pointeeRec->data.LF_CLASS.lfEasy.kind);
|
pointeeRec->data.LF_CLASS.lfEasy.kind);
|
||||||
|
|
||||||
if (typeName) {
|
uint32_t resolved = findUdtDefinitionIndex(pointeeRec->header.kind, typeName);
|
||||||
// Linear scan for the definition (cached after first import)
|
if (resolved != 0) defIndex = resolved;
|
||||||
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
|
||||||
const auto* candidate = tt->get(ti);
|
|
||||||
if (!candidate) continue;
|
|
||||||
if (candidate->header.kind != pointeeRec->header.kind) continue;
|
|
||||||
bool candidateFwd;
|
|
||||||
const char* candidateName;
|
|
||||||
if (candidate->header.kind == TRK::LF_UNION) {
|
|
||||||
candidateFwd = candidate->data.LF_UNION.property.fwdref;
|
|
||||||
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
|
|
||||||
} else {
|
|
||||||
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
|
|
||||||
candidateName = leafName(candidate->data.LF_CLASS.data,
|
|
||||||
candidate->data.LF_CLASS.lfEasy.kind);
|
|
||||||
}
|
|
||||||
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
|
|
||||||
defIndex = ti;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 ||
|
} else if (pointeeRec->header.kind == TRK::LF_PROCEDURE ||
|
||||||
pointeeRec->header.kind == TRK::LF_MFUNCTION) {
|
pointeeRec->header.kind == TRK::LF_MFUNCTION) {
|
||||||
n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64;
|
n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64;
|
||||||
@@ -584,31 +702,10 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
else
|
else
|
||||||
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||||
|
|
||||||
if (typeName) {
|
uint32_t resolved = findUdtDefinitionIndex(rec->header.kind, typeName);
|
||||||
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
if (resolved != 0) defIndex = resolved;
|
||||||
const auto* candidate = tt->get(ti);
|
|
||||||
if (!candidate) continue;
|
|
||||||
if (candidate->header.kind != rec->header.kind) continue;
|
|
||||||
bool candidateFwd;
|
|
||||||
const char* candidateName;
|
|
||||||
if (candidate->header.kind == TRK::LF_UNION) {
|
|
||||||
candidateFwd = candidate->data.LF_UNION.property.fwdref;
|
|
||||||
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
|
|
||||||
} else {
|
|
||||||
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
|
|
||||||
candidateName = leafName(candidate->data.LF_CLASS.data,
|
|
||||||
candidate->data.LF_CLASS.lfEasy.kind);
|
|
||||||
}
|
|
||||||
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
|
|
||||||
defIndex = ti;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t refId = importUDT(defIndex);
|
|
||||||
|
|
||||||
const char* typeName = nullptr;
|
const char* typeName = nullptr;
|
||||||
bool isUnion = (rec->header.kind == TRK::LF_UNION);
|
bool isUnion = (rec->header.kind == TRK::LF_UNION);
|
||||||
if (isUnion)
|
if (isUnion)
|
||||||
@@ -616,6 +713,38 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
else
|
else
|
||||||
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
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;
|
Node n;
|
||||||
n.kind = NodeKind::Struct;
|
n.kind = NodeKind::Struct;
|
||||||
n.name = name;
|
n.name = name;
|
||||||
@@ -707,8 +836,9 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
case TRK::LF_ENUM: {
|
case TRK::LF_ENUM: {
|
||||||
// Map enum to its underlying integer type
|
// Map enum to its underlying integer type, link to enum definition
|
||||||
uint32_t utype = rec->data.LF_ENUM.utype;
|
uint32_t utype = rec->data.LF_ENUM.utype;
|
||||||
|
uint64_t enumNodeId = importEnum(typeIndex);
|
||||||
Node n;
|
Node n;
|
||||||
if (utype < tt->firstIndex()) {
|
if (utype < tt->firstIndex()) {
|
||||||
n.kind = mapPrimitiveType(utype);
|
n.kind = mapPrimitiveType(utype);
|
||||||
@@ -718,6 +848,7 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
n.name = name;
|
n.name = name;
|
||||||
n.parentId = parentId;
|
n.parentId = parentId;
|
||||||
n.offset = offset;
|
n.offset = offset;
|
||||||
|
n.refId = enumNodeId;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -735,16 +866,21 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
|||||||
|
|
||||||
case TRK::LF_BITFIELD: {
|
case TRK::LF_BITFIELD: {
|
||||||
uint32_t underlying = rec->data.LF_BITFIELD.type;
|
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;
|
uint64_t slotSize = 4;
|
||||||
if (underlying < tt->firstIndex()) {
|
if (underlying < tt->firstIndex()) {
|
||||||
NodeKind k = mapPrimitiveType(underlying);
|
NodeKind k = mapPrimitiveType(underlying);
|
||||||
slotSize = sizeForKind(k);
|
slotSize = sizeForKind(k);
|
||||||
}
|
}
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = hexForSize(slotSize);
|
n.kind = NodeKind::Struct;
|
||||||
|
n.classKeyword = QStringLiteral("bitfield");
|
||||||
|
n.elementKind = hexForSize(slotSize);
|
||||||
n.name = name;
|
n.name = name;
|
||||||
n.parentId = parentId;
|
n.parentId = parentId;
|
||||||
n.offset = offset;
|
n.offset = offset;
|
||||||
|
n.bitfieldMembers.append({name, bitPos, bitLen});
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -823,14 +959,27 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
|
|||||||
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
|
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
|
||||||
rec->header.kind == TRK::LF_CLASS ||
|
rec->header.kind == TRK::LF_CLASS ||
|
||||||
rec->header.kind == TRK::LF_UNION);
|
rec->header.kind == TRK::LF_UNION);
|
||||||
if (!isUDT) continue;
|
bool isEnum = (rec->header.kind == TRK::LF_ENUM);
|
||||||
|
if (!isUDT && !isEnum) continue;
|
||||||
|
|
||||||
const char* name = nullptr;
|
const char* name = nullptr;
|
||||||
uint16_t fieldCount = 0;
|
uint16_t fieldCount = 0;
|
||||||
bool isUnion = false;
|
bool isUnion = false;
|
||||||
uint64_t size = 0;
|
uint64_t size = 0;
|
||||||
|
|
||||||
if (rec->header.kind == TRK::LF_UNION) {
|
if (isEnum) {
|
||||||
|
if (rec->data.LF_ENUM.property.fwdref) continue;
|
||||||
|
fieldCount = rec->data.LF_ENUM.count;
|
||||||
|
name = rec->data.LF_ENUM.name;
|
||||||
|
// Size from underlying type
|
||||||
|
uint32_t ut = rec->data.LF_ENUM.utype;
|
||||||
|
if (ut < tt.firstIndex()) {
|
||||||
|
NodeKind ek = mapPrimitiveType(ut);
|
||||||
|
size = sizeForKind(ek);
|
||||||
|
} else {
|
||||||
|
size = 4;
|
||||||
|
}
|
||||||
|
} else if (rec->header.kind == TRK::LF_UNION) {
|
||||||
if (rec->data.LF_UNION.property.fwdref) continue;
|
if (rec->data.LF_UNION.property.fwdref) continue;
|
||||||
isUnion = true;
|
isUnion = true;
|
||||||
fieldCount = rec->data.LF_UNION.count;
|
fieldCount = rec->data.LF_UNION.count;
|
||||||
@@ -856,9 +1005,16 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
|
|||||||
info.size = size;
|
info.size = size;
|
||||||
info.childCount = fieldCount;
|
info.childCount = fieldCount;
|
||||||
info.isUnion = isUnion;
|
info.isUnion = isUnion;
|
||||||
|
info.isEnum = isEnum;
|
||||||
result.append(info);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,14 +1031,34 @@ NodeTree importPdbSelected(const QString& pdbPath,
|
|||||||
ctx.tt = pdb.typeTable;
|
ctx.tt = pdb.typeTable;
|
||||||
|
|
||||||
int total = typeIndices.size();
|
int total = typeIndices.size();
|
||||||
|
int enumDispatched = 0, enumCreated = 0;
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
ctx.importUDT(typeIndices[i]);
|
uint32_t ti = typeIndices[i];
|
||||||
|
const auto* rec = pdb.typeTable->get(ti);
|
||||||
|
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 (progressCb && !progressCb(i + 1, total)) {
|
||||||
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
|
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
|
||||||
return ctx.tree; // return partial result
|
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 (ctx.tree.nodes.isEmpty()) {
|
||||||
if (errorMsg) *errorMsg = QStringLiteral("No types imported");
|
if (errorMsg) *errorMsg = QStringLiteral("No types imported");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ namespace rcx {
|
|||||||
|
|
||||||
struct PdbTypeInfo {
|
struct PdbTypeInfo {
|
||||||
uint32_t typeIndex; // TPI type index
|
uint32_t typeIndex; // TPI type index
|
||||||
QString name; // struct/class/union name
|
QString name; // struct/class/union/enum name
|
||||||
uint64_t size; // sizeof in bytes
|
uint64_t size; // sizeof in bytes
|
||||||
int childCount; // direct member count
|
int childCount; // direct member count
|
||||||
bool isUnion; // union vs struct/class
|
bool isUnion; // union vs struct/class
|
||||||
|
bool isEnum = false; // enum type
|
||||||
};
|
};
|
||||||
|
|
||||||
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
|
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
|
||||||
|
|||||||
@@ -371,7 +371,6 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
|||||||
auto it = classIds.find(ref.className);
|
auto it = classIds.find(ref.className);
|
||||||
if (it != classIds.end()) {
|
if (it != classIds.end()) {
|
||||||
tree.nodes[nodeIdx].refId = it.value();
|
tree.nodes[nodeIdx].refId = it.value();
|
||||||
tree.invalidateIdCache();
|
|
||||||
resolved++;
|
resolved++;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "import_source.h"
|
#include "import_source.h"
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QSet>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@@ -285,13 +286,16 @@ struct ParsedField {
|
|||||||
int commentOffset = -1; // from // 0xNN (-1 = none)
|
int commentOffset = -1; // from // 0xNN (-1 = none)
|
||||||
int bitfieldWidth = -1; // -1 = not a bitfield
|
int bitfieldWidth = -1; // -1 = not a bitfield
|
||||||
QString pointerTarget; // for Type* -> the type name
|
QString pointerTarget; // for Type* -> the type name
|
||||||
|
bool isUnion = false; // union container
|
||||||
|
QVector<ParsedField> unionMembers; // children of union
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ParsedStruct {
|
struct ParsedStruct {
|
||||||
QString name;
|
QString name;
|
||||||
QString keyword; // "struct" or "class"
|
QString keyword; // "struct", "class", or "enum"
|
||||||
QVector<ParsedField> fields;
|
QVector<ParsedField> fields;
|
||||||
int declaredSize = -1; // from static_assert
|
int declaredSize = -1; // from static_assert
|
||||||
|
QVector<QPair<QString, int64_t>> enumValues; // for keyword="enum"
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PendingRef {
|
struct PendingRef {
|
||||||
@@ -378,8 +382,7 @@ struct Parser {
|
|||||||
} else if (checkIdent("typedef")) {
|
} else if (checkIdent("typedef")) {
|
||||||
parseTypedef();
|
parseTypedef();
|
||||||
} else if (checkIdent("enum")) {
|
} else if (checkIdent("enum")) {
|
||||||
skipToSemiOrBrace();
|
parseEnumDef();
|
||||||
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
|
|
||||||
} else if (peek().kind == TokKind::Hash) {
|
} else if (peek().kind == TokKind::Hash) {
|
||||||
// preprocessor (shouldn't reach here if tokenizer skipped them)
|
// preprocessor (shouldn't reach here if tokenizer skipped them)
|
||||||
advance();
|
advance();
|
||||||
@@ -464,12 +467,18 @@ struct Parser {
|
|||||||
// Might be "struct TypeName fieldName;" - fall through to field parsing
|
// Might be "struct TypeName fieldName;" - fall through to field parsing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union: pick first member only
|
// Union: create container with all members
|
||||||
if (checkIdent("union")) {
|
if (checkIdent("union")) {
|
||||||
parseUnion(ps);
|
parseUnion(ps);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enum definition inside struct
|
||||||
|
if (checkIdent("enum")) {
|
||||||
|
parseEnumDef();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Static assert inside struct
|
// Static assert inside struct
|
||||||
if (checkIdent("static_assert")) {
|
if (checkIdent("static_assert")) {
|
||||||
parseStaticAssert();
|
parseStaticAssert();
|
||||||
@@ -489,33 +498,76 @@ struct Parser {
|
|||||||
void parseUnion(ParsedStruct& ps) {
|
void parseUnion(ParsedStruct& ps) {
|
||||||
advance(); // skip "union"
|
advance(); // skip "union"
|
||||||
|
|
||||||
// Optional union name
|
// Optional union tag name (before {)
|
||||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
||||||
advance(); // skip union name
|
advance(); // skip union tag name
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||||
|
|
||||||
// Parse first member of union
|
// Parse ALL members of the union
|
||||||
bool gotFirst = false;
|
ParsedField unionField;
|
||||||
|
unionField.isUnion = true;
|
||||||
|
|
||||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||||
if (!gotFirst) {
|
// Handle nested unions inside this union
|
||||||
ParsedField field;
|
if (checkIdent("union")) {
|
||||||
if (parseField(field)) {
|
// Recurse: create a sub-union ParsedStruct temporarily,
|
||||||
ps.fields.append(field);
|
// then steal its fields as a nested union member
|
||||||
gotFirst = true;
|
ParsedStruct tmp;
|
||||||
} else {
|
parseUnion(tmp);
|
||||||
advance();
|
for (auto& f : tmp.fields)
|
||||||
|
unionField.unionMembers.append(f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle anonymous struct inside union: struct { ... };
|
||||||
|
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
|
||||||
|
advance(); // skip "struct"
|
||||||
|
advance(); // skip "{"
|
||||||
|
int depth = 1;
|
||||||
|
while (peek().kind != TokKind::Eof && depth > 0) {
|
||||||
|
if (peek().kind == TokKind::LBrace) depth++;
|
||||||
|
else if (peek().kind == TokKind::RBrace) depth--;
|
||||||
|
if (depth > 0) advance();
|
||||||
}
|
}
|
||||||
|
if (check(TokKind::RBrace)) advance();
|
||||||
|
if (check(TokKind::Ident)) advance(); // optional field name
|
||||||
|
match(TokKind::Semi);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nested named struct definition inside union
|
||||||
|
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||||
|
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||||
|
parseStructOrForward();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedField field;
|
||||||
|
if (parseField(field)) {
|
||||||
|
unionField.unionMembers.append(field);
|
||||||
} else {
|
} else {
|
||||||
// Skip remaining union members
|
advance();
|
||||||
skipToSemiOrBrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match(TokKind::RBrace);
|
match(TokKind::RBrace);
|
||||||
// Optional field name after union close
|
|
||||||
if (check(TokKind::Ident)) advance();
|
// Optional field name after union close: union { ... } u3;
|
||||||
|
if (check(TokKind::Ident)) {
|
||||||
|
unionField.name = advance().text;
|
||||||
|
}
|
||||||
match(TokKind::Semi);
|
match(TokKind::Semi);
|
||||||
|
|
||||||
|
// Determine offset from first member with a known offset
|
||||||
|
for (const auto& m : unionField.unionMembers) {
|
||||||
|
if (m.commentOffset >= 0) {
|
||||||
|
unionField.commentOffset = m.commentOffset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.fields.append(unionField);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool parseField(ParsedField& field) {
|
bool parseField(ParsedField& field) {
|
||||||
@@ -719,6 +771,90 @@ struct Parser {
|
|||||||
}
|
}
|
||||||
match(TokKind::Semi);
|
match(TokKind::Semi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void parseEnumDef() {
|
||||||
|
advance(); // skip "enum"
|
||||||
|
|
||||||
|
// Optional "class" or "struct" (enum class)
|
||||||
|
if (checkIdent("class") || checkIdent("struct"))
|
||||||
|
advance();
|
||||||
|
|
||||||
|
// Optional name
|
||||||
|
QString name;
|
||||||
|
if (check(TokKind::Ident) && peek(1).kind != TokKind::Semi) {
|
||||||
|
// Could be: enum Name { ... }; or enum Name : Type { ... };
|
||||||
|
// But NOT: enum Name; (forward decl) or enum Name field; (field usage)
|
||||||
|
if (peek(1).kind == TokKind::LBrace || peek(1).kind == TokKind::Colon) {
|
||||||
|
name = advance().text;
|
||||||
|
} else {
|
||||||
|
// Not an enum definition — revert. This might be a field like "enum Foo bar;"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional underlying type: enum Name : uint8_t { ... }
|
||||||
|
if (check(TokKind::Colon)) {
|
||||||
|
advance();
|
||||||
|
parseTypeName(); // skip underlying type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward declaration: enum Name;
|
||||||
|
if (check(TokKind::Semi)) {
|
||||||
|
advance();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||||
|
|
||||||
|
ParsedStruct ps;
|
||||||
|
ps.name = name;
|
||||||
|
ps.keyword = QStringLiteral("enum");
|
||||||
|
|
||||||
|
// Parse enum members: Name [= Value], ...
|
||||||
|
int64_t nextValue = 0;
|
||||||
|
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||||
|
if (!check(TokKind::Ident)) { advance(); continue; }
|
||||||
|
QString memberName = advance().text;
|
||||||
|
int64_t memberValue = nextValue;
|
||||||
|
|
||||||
|
if (check(TokKind::Equals)) {
|
||||||
|
advance();
|
||||||
|
// Parse value: could be number, negative number, or expression
|
||||||
|
bool negative = false;
|
||||||
|
if (peek().kind == TokKind::Other && peek().text == QStringLiteral("-")) {
|
||||||
|
negative = true;
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
if (check(TokKind::Number)) {
|
||||||
|
bool ok;
|
||||||
|
QString numText = peek().text;
|
||||||
|
if (numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||||
|
memberValue = numText.mid(2).toLongLong(&ok, 16);
|
||||||
|
else
|
||||||
|
memberValue = numText.toLongLong(&ok);
|
||||||
|
if (negative) memberValue = -memberValue;
|
||||||
|
advance();
|
||||||
|
} else {
|
||||||
|
// Complex expression — skip to comma or brace
|
||||||
|
while (peek().kind != TokKind::Comma &&
|
||||||
|
peek().kind != TokKind::RBrace &&
|
||||||
|
peek().kind != TokKind::Eof)
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ps.enumValues.append({memberName, memberValue});
|
||||||
|
nextValue = memberValue + 1;
|
||||||
|
|
||||||
|
// Skip comma between members
|
||||||
|
match(TokKind::Comma);
|
||||||
|
}
|
||||||
|
match(TokKind::RBrace);
|
||||||
|
match(TokKind::Semi);
|
||||||
|
|
||||||
|
if (!ps.name.isEmpty())
|
||||||
|
structs.append(ps);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Padding field detection ──
|
// ── Padding field detection ──
|
||||||
@@ -758,6 +894,327 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bitfield grouping: emit a bitfield container with named members ──
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NodeTree builder: recursive field emitter ──
|
||||||
|
|
||||||
|
struct BuildContext {
|
||||||
|
NodeTree& tree;
|
||||||
|
const QHash<QString, TypeInfo>& typeTable;
|
||||||
|
QHash<QString, uint64_t>& classIds;
|
||||||
|
QVector<PendingRef>& pendingRefs;
|
||||||
|
bool useCommentOffsets;
|
||||||
|
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||||
|
};
|
||||||
|
|
||||||
|
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||||
|
const QVector<ParsedField>& fields) {
|
||||||
|
int computedOffset = 0;
|
||||||
|
|
||||||
|
for (int fi = 0; fi < fields.size(); fi++) {
|
||||||
|
const auto& field = fields[fi];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
fi++;
|
||||||
|
}
|
||||||
|
fi--; // compensate for outer loop increment
|
||||||
|
if (totalBits > 0)
|
||||||
|
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;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union container field
|
||||||
|
if (field.isUnion) {
|
||||||
|
int unionOffset;
|
||||||
|
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||||
|
unionOffset = field.commentOffset - baseOffset;
|
||||||
|
else
|
||||||
|
unionOffset = computedOffset;
|
||||||
|
|
||||||
|
Node unionNode;
|
||||||
|
unionNode.kind = NodeKind::Struct;
|
||||||
|
unionNode.classKeyword = QStringLiteral("union");
|
||||||
|
unionNode.name = field.name;
|
||||||
|
unionNode.parentId = parentId;
|
||||||
|
unionNode.offset = unionOffset;
|
||||||
|
unionNode.collapsed = true;
|
||||||
|
|
||||||
|
int unionIdx = ctx.tree.addNode(unionNode);
|
||||||
|
uint64_t unionId = ctx.tree.nodes[unionIdx].id;
|
||||||
|
|
||||||
|
// Build each union member independently so each starts at offset 0
|
||||||
|
int absUnionOffset = baseOffset + unionOffset;
|
||||||
|
for (const auto& member : field.unionMembers) {
|
||||||
|
QVector<ParsedField> single;
|
||||||
|
single.append(member);
|
||||||
|
buildFields(ctx, unionId, absUnionOffset, single);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance computed offset past the union (max member size)
|
||||||
|
int unionSpan = ctx.tree.structSpan(unionId);
|
||||||
|
computedOffset = unionOffset + (unionSpan > 0 ? unionSpan : 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fieldOffset;
|
||||||
|
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||||
|
fieldOffset = field.commentOffset - baseOffset;
|
||||||
|
else
|
||||||
|
fieldOffset = computedOffset;
|
||||||
|
|
||||||
|
// Resolve type
|
||||||
|
auto typeIt = ctx.typeTable.find(field.typeName);
|
||||||
|
bool knownType = typeIt != ctx.typeTable.end();
|
||||||
|
|
||||||
|
// Pointer field
|
||||||
|
if (field.isPointer) {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Pointer64;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.collapsed = true;
|
||||||
|
|
||||||
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
|
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||||
|
|
||||||
|
if (!field.pointerTarget.isEmpty() &&
|
||||||
|
field.pointerTarget != QStringLiteral("void")) {
|
||||||
|
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||||
|
}
|
||||||
|
|
||||||
|
computedOffset = fieldOffset + 8;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum-typed field: emit as UInt32 with refId to enum definition
|
||||||
|
if (!knownType && ctx.enumNames.contains(field.typeName)) {
|
||||||
|
int elemSize = 4;
|
||||||
|
NodeKind elemKind = NodeKind::UInt32;
|
||||||
|
if (!field.arraySizes.isEmpty()) {
|
||||||
|
int totalElements = 1;
|
||||||
|
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Array;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.arrayLen = totalElements;
|
||||||
|
n.elementKind = elemKind;
|
||||||
|
ctx.tree.addNode(n);
|
||||||
|
computedOffset = fieldOffset + totalElements * elemSize;
|
||||||
|
} else {
|
||||||
|
Node n;
|
||||||
|
n.kind = elemKind;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
|
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||||
|
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||||
|
computedOffset = fieldOffset + elemSize;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine base type info
|
||||||
|
NodeKind baseKind = NodeKind::Hex8;
|
||||||
|
int baseSize = 1;
|
||||||
|
bool isStructType = false;
|
||||||
|
|
||||||
|
if (knownType) {
|
||||||
|
baseKind = typeIt->kind;
|
||||||
|
baseSize = typeIt->size;
|
||||||
|
} else {
|
||||||
|
isStructType = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding fields
|
||||||
|
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
||||||
|
int totalSize = baseSize;
|
||||||
|
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
||||||
|
emitHexPadding(ctx.tree, parentId, fieldOffset, totalSize);
|
||||||
|
computedOffset = fieldOffset + totalSize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array fields
|
||||||
|
if (!field.arraySizes.isEmpty() && !isStructType) {
|
||||||
|
int firstDim = field.arraySizes.value(0, 1);
|
||||||
|
if (firstDim <= 0) firstDim = 1;
|
||||||
|
|
||||||
|
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||||
|
field.typeName == QStringLiteral("char")) {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::UTF8;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.strLen = firstDim;
|
||||||
|
ctx.tree.addNode(n);
|
||||||
|
computedOffset = fieldOffset + firstDim;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||||
|
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::UTF16;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.strLen = firstDim;
|
||||||
|
ctx.tree.addNode(n);
|
||||||
|
computedOffset = fieldOffset + firstDim * 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
||||||
|
if (firstDim == 2) {
|
||||||
|
Node n; n.kind = NodeKind::Vec2; n.name = field.name;
|
||||||
|
n.parentId = parentId; n.offset = fieldOffset;
|
||||||
|
ctx.tree.addNode(n); computedOffset = fieldOffset + 8; continue;
|
||||||
|
}
|
||||||
|
if (firstDim == 3) {
|
||||||
|
Node n; n.kind = NodeKind::Vec3; n.name = field.name;
|
||||||
|
n.parentId = parentId; n.offset = fieldOffset;
|
||||||
|
ctx.tree.addNode(n); computedOffset = fieldOffset + 12; continue;
|
||||||
|
}
|
||||||
|
if (firstDim == 4) {
|
||||||
|
Node n; n.kind = NodeKind::Vec4; n.name = field.name;
|
||||||
|
n.parentId = parentId; n.offset = fieldOffset;
|
||||||
|
ctx.tree.addNode(n); computedOffset = fieldOffset + 16; continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
||||||
|
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
||||||
|
Node n; n.kind = NodeKind::Mat4x4; n.name = field.name;
|
||||||
|
n.parentId = parentId; n.offset = fieldOffset;
|
||||||
|
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalElements = 1;
|
||||||
|
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Array;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.arrayLen = totalElements;
|
||||||
|
n.elementKind = baseKind;
|
||||||
|
ctx.tree.addNode(n);
|
||||||
|
computedOffset = fieldOffset + totalElements * baseSize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct-type field
|
||||||
|
if (isStructType) {
|
||||||
|
if (!field.arraySizes.isEmpty()) {
|
||||||
|
int totalElements = 1;
|
||||||
|
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Array;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.arrayLen = totalElements;
|
||||||
|
n.elementKind = NodeKind::Struct;
|
||||||
|
n.structTypeName = field.typeName;
|
||||||
|
n.collapsed = true;
|
||||||
|
|
||||||
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
|
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||||
|
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = NodeKind::Struct;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
n.structTypeName = field.typeName;
|
||||||
|
n.collapsed = true;
|
||||||
|
|
||||||
|
int nodeIdx = ctx.tree.addNode(n);
|
||||||
|
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||||
|
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple primitive field
|
||||||
|
Node n;
|
||||||
|
n.kind = baseKind;
|
||||||
|
n.name = field.name;
|
||||||
|
n.parentId = parentId;
|
||||||
|
n.offset = fieldOffset;
|
||||||
|
ctx.tree.addNode(n);
|
||||||
|
computedOffset = fieldOffset + baseSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Check if any field (or union member) has a comment offset ──
|
||||||
|
|
||||||
|
static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
|
||||||
|
for (const auto& f : fields) {
|
||||||
|
if (f.commentOffset >= 0) return true;
|
||||||
|
if (f.isUnion && hasAnyCommentOffset(f.unionMembers)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── NodeTree builder ──
|
// ── NodeTree builder ──
|
||||||
|
|
||||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||||
@@ -775,7 +1232,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
parser.parse();
|
parser.parse();
|
||||||
|
|
||||||
if (parser.structs.isEmpty()) {
|
if (parser.structs.isEmpty()) {
|
||||||
if (errorMsg) *errorMsg = QStringLiteral("No struct definitions found");
|
if (errorMsg) *errorMsg = QStringLiteral("No struct or enum definitions found");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,13 +1255,19 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
// Determine offset mode: if ANY field in ANY struct has a comment offset, use comment mode
|
// Determine offset mode: if ANY field in ANY struct has a comment offset, use comment mode
|
||||||
bool useCommentOffsets = false;
|
bool useCommentOffsets = false;
|
||||||
for (const auto& ps : parser.structs) {
|
for (const auto& ps : parser.structs) {
|
||||||
for (const auto& f : ps.fields) {
|
if (hasAnyCommentOffset(ps.fields)) { useCommentOffsets = true; break; }
|
||||||
if (f.commentOffset >= 0) { useCommentOffsets = true; break; }
|
|
||||||
}
|
|
||||||
if (useCommentOffsets) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build nodes for each struct
|
// Collect enum type names for field-type detection
|
||||||
|
QSet<QString> enumNames;
|
||||||
|
for (const auto& ps : parser.structs) {
|
||||||
|
if (ps.keyword == QStringLiteral("enum") && !ps.name.isEmpty())
|
||||||
|
enumNames.insert(ps.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames};
|
||||||
|
|
||||||
|
// Build nodes for each struct/enum
|
||||||
for (const auto& ps : parser.structs) {
|
for (const auto& ps : parser.structs) {
|
||||||
Node structNode;
|
Node structNode;
|
||||||
structNode.kind = NodeKind::Struct;
|
structNode.kind = NodeKind::Struct;
|
||||||
@@ -815,222 +1278,21 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
structNode.offset = 0;
|
structNode.offset = 0;
|
||||||
structNode.collapsed = true;
|
structNode.collapsed = true;
|
||||||
|
|
||||||
|
// Enum: store members directly on the node, no child fields
|
||||||
|
if (ps.keyword == QStringLiteral("enum")) {
|
||||||
|
structNode.enumMembers = ps.enumValues;
|
||||||
|
int idx = tree.addNode(structNode);
|
||||||
|
uint64_t nodeId = tree.nodes[idx].id;
|
||||||
|
if (!ps.name.isEmpty())
|
||||||
|
classIds[ps.name] = nodeId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
int structIdx = tree.addNode(structNode);
|
int structIdx = tree.addNode(structNode);
|
||||||
uint64_t structId = tree.nodes[structIdx].id;
|
uint64_t structId = tree.nodes[structIdx].id;
|
||||||
classIds[ps.name] = structId;
|
classIds[ps.name] = structId;
|
||||||
|
|
||||||
int computedOffset = 0;
|
buildFields(ctx, structId, 0, ps.fields);
|
||||||
|
|
||||||
for (const auto& field : ps.fields) {
|
|
||||||
// Skip bitfields
|
|
||||||
if (field.bitfieldWidth >= 0) continue;
|
|
||||||
|
|
||||||
int fieldOffset;
|
|
||||||
if (useCommentOffsets && field.commentOffset >= 0)
|
|
||||||
fieldOffset = field.commentOffset;
|
|
||||||
else
|
|
||||||
fieldOffset = computedOffset;
|
|
||||||
|
|
||||||
// Resolve type
|
|
||||||
auto typeIt = typeTable.find(field.typeName);
|
|
||||||
bool knownType = typeIt != typeTable.end();
|
|
||||||
|
|
||||||
// Pointer field
|
|
||||||
if (field.isPointer) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Pointer64;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.collapsed = true;
|
|
||||||
|
|
||||||
int nodeIdx = tree.addNode(n);
|
|
||||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
|
||||||
|
|
||||||
// If target is not void and not a primitive, defer resolution
|
|
||||||
if (!field.pointerTarget.isEmpty() &&
|
|
||||||
field.pointerTarget != QStringLiteral("void")) {
|
|
||||||
pendingRefs.append({nodeId, field.pointerTarget});
|
|
||||||
}
|
|
||||||
|
|
||||||
computedOffset = fieldOffset + 8; // pointer size
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine base type info
|
|
||||||
NodeKind baseKind = NodeKind::Hex8;
|
|
||||||
int baseSize = 1;
|
|
||||||
bool isStructType = false;
|
|
||||||
|
|
||||||
if (knownType) {
|
|
||||||
baseKind = typeIt->kind;
|
|
||||||
baseSize = typeIt->size;
|
|
||||||
} else {
|
|
||||||
// Unknown type = assume struct reference
|
|
||||||
isStructType = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Padding fields: name-based detection
|
|
||||||
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
|
||||||
int totalSize = baseSize;
|
|
||||||
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
|
||||||
emitHexPadding(tree, structId, fieldOffset, totalSize);
|
|
||||||
computedOffset = fieldOffset + totalSize;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array fields
|
|
||||||
if (!field.arraySizes.isEmpty() && !isStructType) {
|
|
||||||
int firstDim = field.arraySizes.value(0, 1);
|
|
||||||
if (firstDim <= 0) firstDim = 1;
|
|
||||||
|
|
||||||
// Special: char[N] -> UTF8
|
|
||||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
|
||||||
field.typeName == QStringLiteral("char")) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::UTF8;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.strLen = firstDim;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + firstDim;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special: wchar_t[N] -> UTF16
|
|
||||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
|
||||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::UTF16;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.strLen = firstDim;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + firstDim * 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special: float[2] -> Vec2, float[3] -> Vec3, float[4] -> Vec4
|
|
||||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
|
||||||
if (firstDim == 2) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Vec2;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + 8;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (firstDim == 3) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Vec3;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + 12;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (firstDim == 4) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Vec4;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + 16;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special: float[4][4] -> Mat4x4
|
|
||||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
|
||||||
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Mat4x4;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + 64;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic array
|
|
||||||
int totalElements = 1;
|
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Array;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.arrayLen = totalElements;
|
|
||||||
n.elementKind = baseKind;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + totalElements * baseSize;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct-type field (embedded struct or array of structs)
|
|
||||||
if (isStructType) {
|
|
||||||
if (!field.arraySizes.isEmpty()) {
|
|
||||||
// Array of structs
|
|
||||||
int totalElements = 1;
|
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Array;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.arrayLen = totalElements;
|
|
||||||
n.elementKind = NodeKind::Struct;
|
|
||||||
n.structTypeName = field.typeName;
|
|
||||||
n.collapsed = true;
|
|
||||||
|
|
||||||
int nodeIdx = tree.addNode(n);
|
|
||||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
|
||||||
pendingRefs.append({nodeId, field.typeName});
|
|
||||||
|
|
||||||
// For computed offsets: we don't know struct size yet, use 0
|
|
||||||
// The offset will be approximate for unknown struct sizes
|
|
||||||
if (!useCommentOffsets) {
|
|
||||||
// Try to estimate from same-file structs
|
|
||||||
// Can't know size yet since we may not have parsed it
|
|
||||||
// Just advance by 0 (will be corrected by comment offsets if present)
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embedded struct
|
|
||||||
Node n;
|
|
||||||
n.kind = NodeKind::Struct;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
n.structTypeName = field.typeName;
|
|
||||||
n.collapsed = true;
|
|
||||||
|
|
||||||
int nodeIdx = tree.addNode(n);
|
|
||||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
|
||||||
pendingRefs.append({nodeId, field.typeName});
|
|
||||||
// Don't advance computed offset for unknown struct size
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple primitive field
|
|
||||||
Node n;
|
|
||||||
n.kind = baseKind;
|
|
||||||
n.name = field.name;
|
|
||||||
n.parentId = structId;
|
|
||||||
n.offset = fieldOffset;
|
|
||||||
tree.addNode(n);
|
|
||||||
computedOffset = fieldOffset + baseSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply static_assert size: add tail padding if needed
|
// Apply static_assert size: add tail padding if needed
|
||||||
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
||||||
@@ -1056,7 +1318,6 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
|||||||
auto it = classIds.find(ref.className);
|
auto it = classIds.find(ref.className);
|
||||||
if (it != classIds.end()) {
|
if (it != classIds.end()) {
|
||||||
tree.nodes[nodeIdx].refId = it.value();
|
tree.nodes[nodeIdx].refId = it.value();
|
||||||
tree.invalidateIdCache();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
597
src/main.cpp
597
src/main.cpp
@@ -312,6 +312,22 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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
|
||||||
|
patched.palette.setColor(QPalette::HighlightedText,
|
||||||
|
vi->palette.color(QPalette::Text));
|
||||||
|
QProxyStyle::drawControl(element, &patched, p, w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
QProxyStyle::drawControl(element, opt, p, w);
|
QProxyStyle::drawControl(element, opt, p, w);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -427,7 +443,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
// Restore menu bar title case setting (after menus are created)
|
// Restore menu bar title case setting (after menus are created)
|
||||||
{
|
{
|
||||||
QSettings s("Reclass", "Reclass");
|
QSettings s("Reclass", "Reclass");
|
||||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool());
|
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", false).toBool());
|
||||||
if (s.value("showIcon", false).toBool())
|
if (s.value("showIcon", false).toBool())
|
||||||
m_titleBar->setShowIcon(true);
|
m_titleBar->setShowIcon(true);
|
||||||
}
|
}
|
||||||
@@ -442,7 +458,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
|
|
||||||
// Start MCP bridge
|
// Start MCP bridge
|
||||||
m_mcp = new McpBridge(this, this);
|
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();
|
m_mcp->start();
|
||||||
|
|
||||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
||||||
@@ -491,22 +507,19 @@ void MainWindow::createMenus() {
|
|||||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
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);
|
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
m_sourceMenu = file->addMenu("Current Tab So&urce");
|
auto* importMenu = file->addMenu("&Import");
|
||||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
Qt5Qt6AddAction(importMenu, "From &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||||
file->addSeparator();
|
Qt5Qt6AddAction(importMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||||
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb);
|
||||||
file->addSeparator();
|
auto* exportMenu = file->addMenu("E&xport");
|
||||||
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp);
|
||||||
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
Qt5Qt6AddAction(exportMenu, "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);
|
|
||||||
// Examples submenu — scan once at init
|
// Examples submenu — scan once at init
|
||||||
{
|
{
|
||||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||||
if (!rcxFiles.isEmpty()) {
|
if (!rcxFiles.isEmpty()) {
|
||||||
auto* examples = file->addMenu("&Examples");
|
auto* examples = file->addMenu("E&xamples");
|
||||||
for (const QString& fn : rcxFiles) {
|
for (const QString& fn : rcxFiles) {
|
||||||
QString fullPath = exDir.absoluteFilePath(fn);
|
QString fullPath = exDir.absoluteFilePath(fn);
|
||||||
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
|
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
|
||||||
@@ -514,10 +527,7 @@ void MainWindow::createMenus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||||
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);
|
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
||||||
|
|
||||||
@@ -525,13 +535,14 @@ void MainWindow::createMenus() {
|
|||||||
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
||||||
Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
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);
|
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
|
// View
|
||||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||||
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
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();
|
view->addSeparator();
|
||||||
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
||||||
auto* fontGroup = new QActionGroup(this);
|
auto* fontGroup = new QActionGroup(this);
|
||||||
@@ -568,9 +579,38 @@ void MainWindow::createMenus() {
|
|||||||
themeMenu->addSeparator();
|
themeMenu->addSeparator();
|
||||||
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
|
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
|
||||||
|
|
||||||
|
view->addSeparator();
|
||||||
|
auto* actCompact = view->addAction("Compact &Columns");
|
||||||
|
actCompact->setCheckable(true);
|
||||||
|
actCompact->setChecked(settings.value("compactColumns", true).toBool());
|
||||||
|
connect(actCompact, &QAction::triggered, this, [this](bool checked) {
|
||||||
|
QSettings("Reclass", "Reclass").setValue("compactColumns", checked);
|
||||||
|
for (auto& tab : m_tabs)
|
||||||
|
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->addSeparator();
|
||||||
view->addAction(m_workspaceDock->toggleViewAction());
|
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
|
// Plugins
|
||||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||||
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
||||||
@@ -694,6 +734,80 @@ protected:
|
|||||||
void leaveEvent(QEvent*) override { update(); }
|
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 ──
|
// ── Borderless status bar with manual child layout ──
|
||||||
// QStatusBarLayout hardcodes 2px margins that can't be overridden.
|
// QStatusBarLayout hardcodes 2px margins that can't be overridden.
|
||||||
// We bypass it entirely: children are placed manually in resizeEvent,
|
// We bypass it entirely: children are placed manually in resizeEvent,
|
||||||
@@ -701,8 +815,8 @@ protected:
|
|||||||
// children and call manualLayout() to position them.
|
// children and call manualLayout() to position them.
|
||||||
class FlatStatusBar : public QStatusBar {
|
class FlatStatusBar : public QStatusBar {
|
||||||
public:
|
public:
|
||||||
QWidget* tabRow = nullptr; // set by createStatusBar
|
QWidget* tabRow = nullptr; // set by createStatusBar
|
||||||
QLabel* label = nullptr; // set by createStatusBar
|
ShimmerLabel* label = nullptr; // set by createStatusBar
|
||||||
|
|
||||||
void setDividerColor(const QColor& c) { m_div = c; update(); }
|
void setDividerColor(const QColor& c) { m_div = c; update(); }
|
||||||
void setTopLineColor(const QColor& c) { m_top = c; update(); }
|
void setTopLineColor(const QColor& c) { m_top = c; update(); }
|
||||||
@@ -780,7 +894,8 @@ void MainWindow::createStatusBar() {
|
|||||||
auto* sb = new FlatStatusBar;
|
auto* sb = new FlatStatusBar;
|
||||||
setStatusBar(sb);
|
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->setContentsMargins(0, 0, 0, 0);
|
||||||
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
||||||
|
|
||||||
@@ -843,10 +958,42 @@ void MainWindow::createStatusBar() {
|
|||||||
};
|
};
|
||||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnReclass));
|
applyViewTabColors(static_cast<ViewTabButton*>(m_btnReclass));
|
||||||
applyViewTabColors(static_cast<ViewTabButton*>(m_btnRendered));
|
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() {
|
void MainWindow::styleTabCloseButtons() {
|
||||||
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
|
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
|
||||||
@@ -889,6 +1036,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
|||||||
|
|
||||||
// Create editor via controller (parent = tabWidget for ownership)
|
// Create editor via controller (parent = tabWidget for ownership)
|
||||||
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
|
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
|
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
|
||||||
|
|
||||||
// Create per-pane rendered C++ view
|
// Create per-pane rendered C++ view
|
||||||
@@ -988,6 +1137,9 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
// Create the initial split pane
|
// Create the initial split pane
|
||||||
tab.panes.append(createSplitPane(tab));
|
tab.panes.append(createSplitPane(tab));
|
||||||
|
|
||||||
|
// Apply global compact columns setting to new tab
|
||||||
|
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
|
||||||
|
|
||||||
// Give every controller the shared document list for cross-tab type visibility
|
// Give every controller the shared document list for cross-tab type visibility
|
||||||
ctrl->setProjectDocuments(&m_allDocs);
|
ctrl->setProjectDocuments(&m_allDocs);
|
||||||
rebuildAllDocs();
|
rebuildAllDocs();
|
||||||
@@ -1008,19 +1160,17 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
||||||
auto* ap = findActiveSplitPane();
|
auto* ap = findActiveSplitPane();
|
||||||
if (ap && ap->viewMode == VM_Rendered)
|
if (ap && ap->viewMode == VM_Rendered)
|
||||||
m_statusLabel->setText(
|
setAppStatus(
|
||||||
QString("Rendered: %1 %2")
|
QString("Rendered: %1 %2")
|
||||||
.arg(kindToString(node.kind))
|
.arg(kindToString(node.kind))
|
||||||
.arg(node.name));
|
.arg(node.name));
|
||||||
else
|
else
|
||||||
m_statusLabel->setText(
|
setAppStatus(
|
||||||
QString("%1 %2 offset: 0x%3 size: %4 bytes")
|
QString("%1 %2 offset: 0x%3 size: %4 bytes")
|
||||||
.arg(kindToString(node.kind))
|
.arg(kindToString(node.kind))
|
||||||
.arg(node.name)
|
.arg(node.name)
|
||||||
.arg(node.offset, 4, 16, QChar('0'))
|
.arg(node.offset, 4, 16, QChar('0'))
|
||||||
.arg(node.byteSize()));
|
.arg(node.byteSize()));
|
||||||
} else {
|
|
||||||
m_statusLabel->setText("Ready");
|
|
||||||
}
|
}
|
||||||
// Update all rendered panes on selection change
|
// Update all rendered panes on selection change
|
||||||
auto it = m_tabs.find(sub);
|
auto it = m_tabs.find(sub);
|
||||||
@@ -1029,10 +1179,8 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
});
|
});
|
||||||
connect(ctrl, &RcxController::selectionChanged,
|
connect(ctrl, &RcxController::selectionChanged,
|
||||||
this, [this](int count) {
|
this, [this](int count) {
|
||||||
if (count == 0)
|
if (count > 1)
|
||||||
m_statusLabel->setText("Ready");
|
setAppStatus(QString("%1 nodes selected").arg(count));
|
||||||
else if (count > 1)
|
|
||||||
m_statusLabel->setText(QString("%1 nodes selected").arg(count));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update rendered panes and workspace on document changes and undo/redo
|
// Update rendered panes and workspace on document changes and undo/redo
|
||||||
@@ -1101,6 +1249,76 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
|
|||||||
n.offset = i * 8;
|
n.offset = i * 8;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default project: add an example enum and a class with a union
|
||||||
|
if (classKeyword.isEmpty()) {
|
||||||
|
// ── Example enum: _POOL_TYPE ──
|
||||||
|
{
|
||||||
|
Node e;
|
||||||
|
e.kind = NodeKind::Struct;
|
||||||
|
e.name = QStringLiteral("_POOL_TYPE");
|
||||||
|
e.structTypeName = QStringLiteral("_POOL_TYPE");
|
||||||
|
e.classKeyword = QStringLiteral("enum");
|
||||||
|
e.parentId = 0;
|
||||||
|
e.collapsed = false;
|
||||||
|
e.enumMembers = {
|
||||||
|
{QStringLiteral("NonPagedPool"), 0},
|
||||||
|
{QStringLiteral("PagedPool"), 1},
|
||||||
|
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
|
||||||
|
{QStringLiteral("DontUseThisType"), 3},
|
||||||
|
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
|
||||||
|
{QStringLiteral("PagedPoolCacheAligned"), 5},
|
||||||
|
};
|
||||||
|
tree.addNode(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||||
|
{
|
||||||
|
Node cls;
|
||||||
|
cls.kind = NodeKind::Struct;
|
||||||
|
cls.name = QStringLiteral("sample");
|
||||||
|
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||||
|
cls.classKeyword = QStringLiteral("class");
|
||||||
|
cls.parentId = 0;
|
||||||
|
cls.offset = 0;
|
||||||
|
int ci = tree.addNode(cls);
|
||||||
|
uint64_t clsId = tree.nodes[ci].id;
|
||||||
|
|
||||||
|
// Field: uint32_t Type at offset 0
|
||||||
|
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||||
|
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||||
|
// Field: uint32_t Size at offset 4
|
||||||
|
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||||
|
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||||
|
|
||||||
|
// Union at offset 8
|
||||||
|
{
|
||||||
|
Node u;
|
||||||
|
u.kind = NodeKind::Struct;
|
||||||
|
u.name = QStringLiteral("Data");
|
||||||
|
u.structTypeName = QStringLiteral("Data");
|
||||||
|
u.classKeyword = QStringLiteral("union");
|
||||||
|
u.parentId = clsId;
|
||||||
|
u.offset = 8;
|
||||||
|
int ui = tree.addNode(u);
|
||||||
|
uint64_t uId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
// Union member: uint64_t AsLong
|
||||||
|
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||||
|
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||||
|
// Union member: void* AsPointer
|
||||||
|
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||||
|
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||||
|
// Union member: float[2] AsFloat2
|
||||||
|
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||||
|
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field: void* Next at offset 16
|
||||||
|
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||||
|
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::newClass() {
|
void MainWindow::newClass() {
|
||||||
@@ -1185,6 +1403,73 @@ static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
|||||||
n.offset = off;
|
n.offset = off;
|
||||||
tree.addNode(n);
|
tree.addNode(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Example enum: _POOL_TYPE ──
|
||||||
|
{
|
||||||
|
Node e;
|
||||||
|
e.kind = NodeKind::Struct;
|
||||||
|
e.name = QStringLiteral("_POOL_TYPE");
|
||||||
|
e.structTypeName = QStringLiteral("_POOL_TYPE");
|
||||||
|
e.classKeyword = QStringLiteral("enum");
|
||||||
|
e.parentId = 0;
|
||||||
|
e.collapsed = false;
|
||||||
|
e.enumMembers = {
|
||||||
|
{QStringLiteral("NonPagedPool"), 0},
|
||||||
|
{QStringLiteral("PagedPool"), 1},
|
||||||
|
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
|
||||||
|
{QStringLiteral("DontUseThisType"), 3},
|
||||||
|
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
|
||||||
|
{QStringLiteral("PagedPoolCacheAligned"), 5},
|
||||||
|
};
|
||||||
|
tree.addNode(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||||
|
{
|
||||||
|
Node cls;
|
||||||
|
cls.kind = NodeKind::Struct;
|
||||||
|
cls.name = QStringLiteral("sample");
|
||||||
|
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||||
|
cls.classKeyword = QStringLiteral("class");
|
||||||
|
cls.parentId = 0;
|
||||||
|
cls.offset = 0;
|
||||||
|
int ci = tree.addNode(cls);
|
||||||
|
uint64_t clsId = tree.nodes[ci].id;
|
||||||
|
|
||||||
|
// Field: uint32_t Type at offset 0
|
||||||
|
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||||
|
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||||
|
// Field: uint32_t Size at offset 4
|
||||||
|
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||||
|
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||||
|
|
||||||
|
// Union at offset 8
|
||||||
|
{
|
||||||
|
Node u;
|
||||||
|
u.kind = NodeKind::Struct;
|
||||||
|
u.name = QStringLiteral("Data");
|
||||||
|
u.structTypeName = QStringLiteral("Data");
|
||||||
|
u.classKeyword = QStringLiteral("union");
|
||||||
|
u.parentId = clsId;
|
||||||
|
u.offset = 8;
|
||||||
|
int ui = tree.addNode(u);
|
||||||
|
uint64_t uId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
// Union member: uint64_t AsLong
|
||||||
|
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||||
|
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||||
|
// Union member: void* AsPointer
|
||||||
|
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||||
|
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||||
|
// Union member: float[2] AsFloat2
|
||||||
|
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||||
|
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field: void* Next at offset 16
|
||||||
|
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||||
|
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::selfTest() {
|
void MainWindow::selfTest() {
|
||||||
@@ -1255,7 +1540,9 @@ void MainWindow::removeNode() {
|
|||||||
QSet<uint64_t> ids = ctrl->selectedIds();
|
QSet<uint64_t> ids = ctrl->selectedIds();
|
||||||
QVector<int> indices;
|
QVector<int> indices;
|
||||||
for (uint64_t id : ids) {
|
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 (idx >= 0) indices.append(idx);
|
||||||
}
|
}
|
||||||
if (indices.size() > 1)
|
if (indices.size() > 1)
|
||||||
@@ -1360,11 +1647,11 @@ void MainWindow::toggleMcp() {
|
|||||||
if (m_mcp->isRunning()) {
|
if (m_mcp->isRunning()) {
|
||||||
m_mcp->stop();
|
m_mcp->stop();
|
||||||
m_mcpAction->setText("Start &MCP Server");
|
m_mcpAction->setText("Start &MCP Server");
|
||||||
m_statusLabel->setText("MCP server stopped");
|
setAppStatus("MCP server stopped");
|
||||||
} else {
|
} else {
|
||||||
m_mcp->start();
|
m_mcp->start();
|
||||||
m_mcpAction->setText("Stop &MCP Server");
|
m_mcpAction->setText("Stop &MCP Server");
|
||||||
m_statusLabel->setText("MCP server listening on pipe: ReclassMcpBridge");
|
setAppStatus("MCP server listening on pipe: ReclassMcpBridge");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,15 +1666,21 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
// Update border overlay color
|
// Update border overlay color
|
||||||
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||||
|
|
||||||
// MDI area tabs
|
// MDI area tabs — text color + height handled by MenuBarStyle QProxyStyle
|
||||||
m_mdiArea->setStyleSheet(QStringLiteral(
|
m_mdiArea->setStyleSheet(QStringLiteral(
|
||||||
"QTabBar::tab {"
|
"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:selected { background: %2; }"
|
||||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
"QTabBar::tab:hover { background: %3; }")
|
||||||
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
.arg(theme.background.name(), theme.backgroundAlt.name(), theme.hover.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
|
// Re-style ✕ close buttons on MDI tabs
|
||||||
styleTabCloseButtons();
|
styleTabCloseButtons();
|
||||||
@@ -1424,16 +1717,32 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
||||||
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
||||||
|
|
||||||
// Workspace tree: text color matches menu bar
|
// Workspace tree: colors from theme (selection + text)
|
||||||
if (m_workspaceTree) {
|
if (m_workspaceTree) {
|
||||||
QPalette tp = m_workspaceTree->palette();
|
QPalette tp = m_workspaceTree->palette();
|
||||||
tp.setColor(QPalette::Text, theme.textDim);
|
tp.setColor(QPalette::Text, theme.textDim);
|
||||||
|
tp.setColor(QPalette::Highlight, theme.hover);
|
||||||
|
tp.setColor(QPalette::HighlightedText, theme.text);
|
||||||
m_workspaceTree->setPalette(tp);
|
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 label + close button
|
// Dock titlebar: restyle via palette + close button
|
||||||
if (m_dockTitleLabel)
|
if (m_dockTitleLabel) {
|
||||||
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
QPalette lp = m_dockTitleLabel->palette();
|
||||||
|
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||||
|
m_dockTitleLabel->setPalette(lp);
|
||||||
|
}
|
||||||
|
if (auto* titleBar = m_workspaceDock ? m_workspaceDock->titleBarWidget() : nullptr) {
|
||||||
|
QPalette tbPal = titleBar->palette();
|
||||||
|
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||||
|
titleBar->setPalette(tbPal);
|
||||||
|
}
|
||||||
if (m_dockCloseBtn)
|
if (m_dockCloseBtn)
|
||||||
m_dockCloseBtn->setStyleSheet(QStringLiteral(
|
m_dockCloseBtn->setStyleSheet(QStringLiteral(
|
||||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||||
@@ -1493,7 +1802,7 @@ void MainWindow::showOptionsDialog() {
|
|||||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
||||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", 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();
|
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
|
|
||||||
OptionsDialog dlg(current, this);
|
OptionsDialog dlg(current, this);
|
||||||
@@ -1706,7 +2015,8 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
|
|||||||
QSet<uint64_t> selIds = tab.ctrl->selectedIds();
|
QSet<uint64_t> selIds = tab.ctrl->selectedIds();
|
||||||
if (selIds.size() >= 1) {
|
if (selIds.size() >= 1) {
|
||||||
uint64_t selId = *selIds.begin();
|
uint64_t selId = *selIds.begin();
|
||||||
selId &= ~kFooterIdBit;
|
selId &= ~(kFooterIdBit | kArrayElemBit | kArrayElemMask
|
||||||
|
| kMemberBit | kMemberSubMask);
|
||||||
rootId = findRootStructForNode(tab.doc->tree, selId);
|
rootId = findRootStructForNode(tab.doc->tree, selId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1769,7 +2079,7 @@ void MainWindow::exportCpp() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file.write(text.toUtf8());
|
file.write(text.toUtf8());
|
||||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
setAppStatus("Exported to " + QFileInfo(path).fileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Export ReClass XML ──
|
// ── Export ReClass XML ──
|
||||||
@@ -1793,7 +2103,7 @@ void MainWindow::exportReclassXmlAction() {
|
|||||||
for (const auto& n : tab->doc->tree.nodes)
|
for (const auto& n : tab->doc->tree.nodes)
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
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()));
|
.arg(classCount).arg(QFileInfo(path).fileName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1824,7 +2134,7 @@ void MainWindow::importReclassXml() {
|
|||||||
m_mdiArea->closeAllSubWindows();
|
m_mdiArea->closeAllSubWindows();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1873,7 +2183,8 @@ void MainWindow::importFromSource() {
|
|||||||
m_mdiArea->closeAllSubWindows();
|
m_mdiArea->closeAllSubWindows();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
m_workspaceDock->show();
|
||||||
|
setAppStatus(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Import PDB ──
|
// ── Import PDB ──
|
||||||
@@ -1922,7 +2233,8 @@ void MainWindow::importPdb() {
|
|||||||
m_mdiArea->closeAllSubWindows();
|
m_mdiArea->closeAllSubWindows();
|
||||||
createTab(doc);
|
createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
m_workspaceDock->show();
|
||||||
|
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||||
.arg(classCount).arg(QFileInfo(pdbPath).fileName()));
|
.arg(classCount).arg(QFileInfo(pdbPath).fileName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1934,30 +2246,91 @@ void MainWindow::showTypeAliasesDialog() {
|
|||||||
|
|
||||||
QDialog dlg(this);
|
QDialog dlg(this);
|
||||||
dlg.setWindowTitle("Type Aliases");
|
dlg.setWindowTitle("Type Aliases");
|
||||||
dlg.resize(500, 400);
|
dlg.resize(400, 380);
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout(&dlg);
|
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);
|
auto* table = new QTableWidget(&dlg);
|
||||||
table->setColumnCount(2);
|
table->setColumnCount(2);
|
||||||
table->setHorizontalHeaderLabels({"NodeKind", "Alias (C type)"});
|
table->horizontalHeader()->setVisible(false);
|
||||||
table->horizontalHeader()->setStretchLastSection(true);
|
table->horizontalHeader()->setStretchLastSection(true);
|
||||||
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
table->verticalHeader()->setVisible(false);
|
||||||
|
|
||||||
// Populate with all NodeKind entries
|
// Skip types that nobody aliases (Vec, Mat, Struct, Array)
|
||||||
int rowCount = static_cast<int>(std::size(kKindMeta));
|
auto shouldSkip = [](NodeKind k) {
|
||||||
table->setRowCount(rowCount);
|
return k == NodeKind::Vec2 || k == NodeKind::Vec3
|
||||||
for (int i = 0; i < rowCount; i++) {
|
|| k == NodeKind::Vec4 || k == NodeKind::Mat4x4
|
||||||
const auto& meta = kKindMeta[i];
|
|| 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));
|
auto* kindItem = new QTableWidgetItem(QString::fromLatin1(meta.name));
|
||||||
kindItem->setFlags(kindItem->flags() & ~Qt::ItemIsEditable);
|
kindItem->setFlags(kindItem->flags() & ~Qt::ItemIsEditable);
|
||||||
table->setItem(i, 0, kindItem);
|
table->setItem(row, 0, kindItem);
|
||||||
|
|
||||||
QString alias = tab->doc->typeAliases.value(meta.kind);
|
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);
|
layout->addWidget(table);
|
||||||
|
|
||||||
auto* buttons = new QDialogButtonBox(
|
auto* buttons = new QDialogButtonBox(
|
||||||
@@ -1971,10 +2344,10 @@ void MainWindow::showTypeAliasesDialog() {
|
|||||||
|
|
||||||
// Collect new aliases
|
// Collect new aliases
|
||||||
QHash<NodeKind, QString> newAliases;
|
QHash<NodeKind, QString> newAliases;
|
||||||
for (int i = 0; i < rowCount; i++) {
|
for (int row = 0; row < rowMap.size(); row++) {
|
||||||
QString val = table->item(i, 1)->text().trimmed();
|
QString val = table->item(row, 1)->text().trimmed();
|
||||||
if (!val.isEmpty())
|
if (!val.isEmpty())
|
||||||
newAliases[kKindMeta[i].kind] = val;
|
newAliases[kKindMeta[rowMap[row]].kind] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
tab->doc->typeAliases = newAliases;
|
tab->doc->typeAliases = newAliases;
|
||||||
@@ -2051,10 +2424,11 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
m_mdiArea->closeAllSubWindows();
|
m_mdiArea->closeAllSubWindows();
|
||||||
auto* sub = createTab(doc);
|
auto* sub = createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
|
m_workspaceDock->show();
|
||||||
int classCount = 0;
|
int classCount = 0;
|
||||||
for (const auto& n : doc->tree.nodes)
|
for (const auto& n : doc->tree.nodes)
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
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()));
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
@@ -2071,6 +2445,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
|
|
||||||
auto* sub = createTab(doc);
|
auto* sub = createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
|
m_workspaceDock->show();
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2101,7 +2476,7 @@ void MainWindow::project_close(QMdiSubWindow* sub) {
|
|||||||
// ── Workspace Dock ──
|
// ── Workspace Dock ──
|
||||||
|
|
||||||
void MainWindow::createWorkspaceDock() {
|
void MainWindow::createWorkspaceDock() {
|
||||||
m_workspaceDock = new QDockWidget("Project Tree", this);
|
m_workspaceDock = new QDockWidget("Project", this);
|
||||||
m_workspaceDock->setObjectName("WorkspaceDock");
|
m_workspaceDock->setObjectName("WorkspaceDock");
|
||||||
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||||
m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||||
@@ -2111,17 +2486,22 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
|
|
||||||
auto* titleBar = new QWidget(m_workspaceDock);
|
auto* titleBar = new QWidget(m_workspaceDock);
|
||||||
|
titleBar->setFixedHeight(24);
|
||||||
|
titleBar->setAutoFillBackground(true);
|
||||||
|
{
|
||||||
|
QPalette tbPal = titleBar->palette();
|
||||||
|
tbPal.setColor(QPalette::Window, t.backgroundAlt);
|
||||||
|
titleBar->setPalette(tbPal);
|
||||||
|
}
|
||||||
auto* layout = new QHBoxLayout(titleBar);
|
auto* layout = new QHBoxLayout(titleBar);
|
||||||
layout->setContentsMargins(6, 2, 2, 2);
|
layout->setContentsMargins(6, 2, 2, 2);
|
||||||
layout->setSpacing(0);
|
layout->setSpacing(0);
|
||||||
|
|
||||||
m_dockTitleLabel = new QLabel("Project Tree", titleBar);
|
m_dockTitleLabel = new QLabel("Project", titleBar);
|
||||||
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(t.textDim.name()));
|
|
||||||
{
|
{
|
||||||
QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
QPalette lp = m_dockTitleLabel->palette();
|
||||||
QFont f(fontName, 12);
|
lp.setColor(QPalette::WindowText, t.textDim);
|
||||||
f.setFixedPitch(true);
|
m_dockTitleLabel->setPalette(lp);
|
||||||
m_dockTitleLabel->setFont(f);
|
|
||||||
}
|
}
|
||||||
layout->addWidget(m_dockTitleLabel);
|
layout->addWidget(m_dockTitleLabel);
|
||||||
|
|
||||||
@@ -2141,15 +2521,59 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
m_workspaceDock->setTitleBarWidget(titleBar);
|
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 = new QStandardItemModel(this);
|
||||||
m_workspaceModel->setHorizontalHeaderLabels({"Name"});
|
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->setHeaderHidden(true);
|
||||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||||
m_workspaceTree->setMouseTracking(true);
|
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();
|
||||||
|
QPalette tp = m_workspaceTree->palette();
|
||||||
|
tp.setColor(QPalette::Text, t.textDim);
|
||||||
|
tp.setColor(QPalette::Highlight, t.hover);
|
||||||
|
tp.setColor(QPalette::HighlightedText, t.text);
|
||||||
|
m_workspaceTree->setPalette(tp);
|
||||||
|
}
|
||||||
|
|
||||||
|
dockLayout->addWidget(m_workspaceTree);
|
||||||
|
|
||||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||||
@@ -2252,7 +2676,7 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
m_workspaceDock->setWidget(m_workspaceTree);
|
m_workspaceDock->setWidget(dockContainer);
|
||||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||||
m_workspaceDock->hide();
|
m_workspaceDock->hide();
|
||||||
|
|
||||||
@@ -2273,12 +2697,23 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
|
|
||||||
m_mdiArea->setActiveSubWindow(sub);
|
m_mdiArea->setActiveSubWindow(sub);
|
||||||
|
|
||||||
// Type/Enum node: navigate to it
|
|
||||||
auto& tree = m_tabs[sub].doc->tree;
|
auto& tree = m_tabs[sub].doc->tree;
|
||||||
int ni = tree.indexOfId(structId);
|
int ni = tree.indexOfId(structId);
|
||||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
if (ni < 0) return;
|
||||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
|
||||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
// 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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2298,7 +2733,7 @@ void MainWindow::rebuildWorkspaceModel() {
|
|||||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||||
}
|
}
|
||||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
||||||
m_workspaceTree->expandToDepth(1);
|
m_workspaceTree->expandToDepth(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::populateSourceMenu() {
|
void MainWindow::populateSourceMenu() {
|
||||||
@@ -2417,7 +2852,7 @@ void MainWindow::showPluginsDialog() {
|
|||||||
if (!path.isEmpty()) {
|
if (!path.isEmpty()) {
|
||||||
if (m_pluginManager.LoadPluginFromPath(path)) {
|
if (m_pluginManager.LoadPluginFromPath(path)) {
|
||||||
refreshList();
|
refreshList();
|
||||||
m_statusLabel->setText("Plugin loaded successfully");
|
setAppStatus("Plugin loaded successfully");
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::warning(&dialog, "Failed to Load Plugin",
|
QMessageBox::warning(&dialog, "Failed to Load Plugin",
|
||||||
"Could not load the selected plugin.\nCheck the console for details.");
|
"Could not load the selected plugin.\nCheck the console for details.");
|
||||||
@@ -2443,7 +2878,7 @@ void MainWindow::showPluginsDialog() {
|
|||||||
if (reply == QMessageBox::Yes) {
|
if (reply == QMessageBox::Yes) {
|
||||||
if (m_pluginManager.UnloadPlugin(pluginName)) {
|
if (m_pluginManager.UnloadPlugin(pluginName)) {
|
||||||
refreshList();
|
refreshList();
|
||||||
m_statusLabel->setText("Plugin unloaded");
|
setAppStatus("Plugin unloaded");
|
||||||
} else {
|
} else {
|
||||||
QMessageBox::warning(&dialog, "Failed to Unload",
|
QMessageBox::warning(&dialog, "Failed to Unload",
|
||||||
"Could not unload the selected plugin.");
|
"Could not unload the selected plugin.");
|
||||||
|
|||||||
@@ -11,14 +11,18 @@
|
|||||||
#include <QDockWidget>
|
#include <QDockWidget>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QTimer>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
class McpBridge;
|
class McpBridge;
|
||||||
|
class ShimmerLabel;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class MainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -59,6 +63,11 @@ private slots:
|
|||||||
void showOptionsDialog();
|
void showOptionsDialog();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// Status bar helpers — separate app / MCP channels
|
||||||
|
void setAppStatus(const QString& text);
|
||||||
|
void setMcpStatus(const QString& text);
|
||||||
|
void clearMcpStatus();
|
||||||
|
|
||||||
// Project Lifecycle API
|
// Project Lifecycle API
|
||||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
||||||
QMdiSubWindow* project_open(const QString& path = {});
|
QMdiSubWindow* project_open(const QString& path = {});
|
||||||
@@ -69,7 +78,10 @@ private:
|
|||||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||||
|
|
||||||
QMdiArea* m_mdiArea;
|
QMdiArea* m_mdiArea;
|
||||||
QLabel* m_statusLabel;
|
ShimmerLabel* m_statusLabel;
|
||||||
|
QString m_appStatus;
|
||||||
|
bool m_mcpBusy = false;
|
||||||
|
QTimer* m_mcpClearTimer = nullptr;
|
||||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
QButtonGroup* m_viewBtnGroup = nullptr;
|
||||||
QPushButton* m_btnReclass = nullptr;
|
QPushButton* m_btnReclass = nullptr;
|
||||||
QPushButton* m_btnRendered = nullptr;
|
QPushButton* m_btnRendered = nullptr;
|
||||||
@@ -127,11 +139,13 @@ private:
|
|||||||
RcxEditor* activePaneEditor();
|
RcxEditor* activePaneEditor();
|
||||||
|
|
||||||
// Workspace dock
|
// Workspace dock
|
||||||
QDockWidget* m_workspaceDock = nullptr;
|
QDockWidget* m_workspaceDock = nullptr;
|
||||||
QTreeView* m_workspaceTree = nullptr;
|
QTreeView* m_workspaceTree = nullptr;
|
||||||
QStandardItemModel* m_workspaceModel = nullptr;
|
QStandardItemModel* m_workspaceModel = nullptr;
|
||||||
QLabel* m_dockTitleLabel = nullptr;
|
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||||
QToolButton* m_dockCloseBtn = nullptr;
|
QLineEdit* m_workspaceSearch = nullptr;
|
||||||
|
QLabel* m_dockTitleLabel = nullptr;
|
||||||
|
QToolButton* m_dockCloseBtn = nullptr;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel();
|
void rebuildWorkspaceModel();
|
||||||
void updateBorderColor(const QColor& color);
|
void updateBorderColor(const QColor& color);
|
||||||
|
|||||||
@@ -170,9 +170,15 @@ void McpBridge::processLine(const QByteArray& line) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (method == "initialize") {
|
if (method == "initialize") {
|
||||||
|
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
||||||
|
QCoreApplication::processEvents();
|
||||||
sendJson(handleInitialize(id, req.value("params").toObject()));
|
sendJson(handleInitialize(id, req.value("params").toObject()));
|
||||||
|
m_mainWindow->clearMcpStatus();
|
||||||
} else if (method == "tools/list") {
|
} else if (method == "tools/list") {
|
||||||
|
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
||||||
|
QCoreApplication::processEvents();
|
||||||
sendJson(handleToolsList(id));
|
sendJson(handleToolsList(id));
|
||||||
|
m_mainWindow->clearMcpStatus();
|
||||||
} else if (method == "tools/call") {
|
} else if (method == "tools/call") {
|
||||||
sendJson(handleToolsCall(id, req.value("params").toObject()));
|
sendJson(handleToolsCall(id, req.value("params").toObject()));
|
||||||
} else {
|
} else {
|
||||||
@@ -211,20 +217,29 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
// 1. project.state
|
// 1. project.state
|
||||||
tools.append(QJsonObject{
|
tools.append(QJsonObject{
|
||||||
{"name", "project.state"},
|
{"name", "project.state"},
|
||||||
{"description", "Returns project state: node tree, base address, sources, provider info. "
|
{"description", "Returns project state with paginated node tree. "
|
||||||
"Use depth/parentId to avoid dumping the whole tree. "
|
"Responses return max 'limit' nodes (default 50). "
|
||||||
"Call with depth:1 first to see top-level structs, then drill in with parentId."},
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||||
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
{"description", "MDI tab index (0-based). Omit for active tab."}}},
|
||||||
{"depth", QJsonObject{{"type", "integer"},
|
{"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"},
|
{"parentId", QJsonObject{{"type", "string"},
|
||||||
{"description", "Only return children of this node."}}},
|
{"description", "Only return children of this node."}}},
|
||||||
{"includeTree", QJsonObject{{"type", "boolean"},
|
{"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. "
|
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
|
||||||
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
||||||
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"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}});
|
return okReply(id, QJsonObject{{"tools", tools}});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +406,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
|||||||
QString toolName = params.value("name").toString();
|
QString toolName = params.value("name").toString();
|
||||||
QJsonObject args = params.value("arguments").toObject();
|
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;
|
QJsonObject result;
|
||||||
if (toolName == "project.state") result = toolProjectState(args);
|
if (toolName == "project.state") result = toolProjectState(args);
|
||||||
else if (toolName == "tree.apply") result = toolTreeApply(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 == "hex.write") result = toolHexWrite(args);
|
||||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||||
else if (toolName == "ui.action") result = toolUiAction(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);
|
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||||
|
|
||||||
|
m_mainWindow->clearMcpStatus();
|
||||||
|
|
||||||
return okReply(id, result);
|
return okReply(id, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +481,9 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
|
|
||||||
int maxDepth = args.value("depth").toInt(1);
|
int maxDepth = args.value("depth").toInt(1);
|
||||||
bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true;
|
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();
|
QString parentIdStr = args.value("parentId").toString();
|
||||||
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
|
uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong();
|
||||||
|
|
||||||
@@ -481,6 +529,7 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
state["modified"] = doc->modified;
|
state["modified"] = doc->modified;
|
||||||
state["undoAvailable"] = doc->undoStack.canUndo();
|
state["undoAvailable"] = doc->undoStack.canUndo();
|
||||||
state["redoAvailable"] = doc->undoStack.canRedo();
|
state["redoAvailable"] = doc->undoStack.canRedo();
|
||||||
|
state["statusText"] = m_mainWindow->m_appStatus;
|
||||||
|
|
||||||
// Filtered tree: only emit nodes up to maxDepth from the filter root
|
// Filtered tree: only emit nodes up to maxDepth from the filter root
|
||||||
if (includeTree) {
|
if (includeTree) {
|
||||||
@@ -489,12 +538,15 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
for (int i = 0; i < tree.nodes.size(); i++)
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
childMap[tree.nodes[i].parentId].append(i);
|
childMap[tree.nodes[i].parentId].append(i);
|
||||||
|
|
||||||
// BFS from filterParentId, respecting maxDepth
|
// BFS from filterParentId, respecting maxDepth + pagination
|
||||||
QJsonArray nodeArr;
|
QJsonArray nodeArr;
|
||||||
struct QueueEntry { uint64_t parentId; int depth; };
|
struct QueueEntry { uint64_t parentId; int depth; };
|
||||||
QVector<QueueEntry> queue;
|
QVector<QueueEntry> queue;
|
||||||
queue.append({filterParentId, 0});
|
queue.append({filterParentId, 0});
|
||||||
|
|
||||||
|
int totalCount = 0; // total nodes that match depth filter
|
||||||
|
int emitted = 0;
|
||||||
|
|
||||||
while (!queue.isEmpty()) {
|
while (!queue.isEmpty()) {
|
||||||
auto entry = queue.takeFirst();
|
auto entry = queue.takeFirst();
|
||||||
if (entry.depth > maxDepth) continue;
|
if (entry.depth > maxDepth) continue;
|
||||||
@@ -502,13 +554,47 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
const auto& kids = childMap.value(entry.parentId);
|
const auto& kids = childMap.value(entry.parentId);
|
||||||
for (int ci : kids) {
|
for (int ci : kids) {
|
||||||
const Node& n = tree.nodes[ci];
|
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();
|
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
|
// Add computed size for containers
|
||||||
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
|
if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) {
|
||||||
nj["computedSize"] = tree.structSpan(n.id, &childMap);
|
nj["computedSize"] = tree.structSpan(n.id, &childMap);
|
||||||
nj["childCount"] = childMap.value(n.id).size();
|
nj["childCount"] = childMap.value(n.id).size();
|
||||||
}
|
}
|
||||||
nodeArr.append(nj);
|
nodeArr.append(nj);
|
||||||
|
emitted++;
|
||||||
|
|
||||||
// Enqueue children if we haven't hit depth limit
|
// Enqueue children if we haven't hit depth limit
|
||||||
if (entry.depth + 1 <= maxDepth)
|
if (entry.depth + 1 <= maxDepth)
|
||||||
@@ -520,6 +606,10 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) {
|
|||||||
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
|
treeObj["baseAddress"] = QString::number(tree.baseAddress, 16);
|
||||||
treeObj["nextId"] = QString::number(tree.m_nextId);
|
treeObj["nextId"] = QString::number(tree.m_nextId);
|
||||||
treeObj["nodes"] = nodeArr;
|
treeObj["nodes"] = nodeArr;
|
||||||
|
treeObj["returned"] = emitted;
|
||||||
|
treeObj["total"] = totalCount;
|
||||||
|
if (emitted < totalCount)
|
||||||
|
treeObj["nextOffset"] = offset + emitted;
|
||||||
state["tree"] = treeObj;
|
state["tree"] = treeObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,7 +1046,7 @@ QJsonObject McpBridge::toolStatusSet(const QJsonObject& args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (target == "statusBar" || target == "both") {
|
if (target == "statusBar" || target == "both") {
|
||||||
m_mainWindow->m_statusLabel->setText(text);
|
m_mainWindow->setAppStatus(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeTextResult("Status set: " + text);
|
return makeTextResult("Status set: " + text);
|
||||||
@@ -1004,7 +1094,24 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
|||||||
if (action == "export_cpp") {
|
if (action == "export_cpp") {
|
||||||
if (!doc) return makeTextResult("No active tab", true);
|
if (!doc) return makeTextResult("No active tab", true);
|
||||||
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
|
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);
|
return makeTextResult(code);
|
||||||
}
|
}
|
||||||
if (action == "save_file") {
|
if (action == "save_file") {
|
||||||
@@ -1053,6 +1160,70 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
|||||||
return makeTextResult("Unknown action: " + action, true);
|
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)
|
// Notifications (call from MainWindow/Controller hooks)
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ private:
|
|||||||
QJsonObject toolHexWrite(const QJsonObject& args);
|
QJsonObject toolHexWrite(const QJsonObject& args);
|
||||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||||
QJsonObject toolUiAction(const QJsonObject& args);
|
QJsonObject toolUiAction(const QJsonObject& args);
|
||||||
|
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct OptionsResult {
|
|||||||
bool menuBarTitleCase = true;
|
bool menuBarTitleCase = true;
|
||||||
bool showIcon = false;
|
bool showIcon = false;
|
||||||
bool safeMode = false;
|
bool safeMode = false;
|
||||||
bool autoStartMcp = false;
|
bool autoStartMcp = true;
|
||||||
int refreshMs = 660;
|
int refreshMs = 660;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"textDim": "#858585",
|
"textDim": "#858585",
|
||||||
"textMuted": "#585858",
|
"textMuted": "#585858",
|
||||||
"textFaint": "#505050",
|
"textFaint": "#505050",
|
||||||
"hover": "#1e1e1e",
|
"hover": "#2a2a2a",
|
||||||
"selected": "#1e1e1e",
|
"selected": "#2a2d2e",
|
||||||
"selection": "#2b2b2b",
|
"selection": "#2b2b2b",
|
||||||
"syntaxKeyword": "#569cd6",
|
"syntaxKeyword": "#569cd6",
|
||||||
"syntaxNumber": "#b5cea8",
|
"syntaxNumber": "#b5cea8",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ private:
|
|||||||
QToolButton* m_btnClose = nullptr;
|
QToolButton* m_btnClose = nullptr;
|
||||||
|
|
||||||
Theme m_theme;
|
Theme m_theme;
|
||||||
bool m_titleCase = true;
|
bool m_titleCase = false;
|
||||||
|
|
||||||
QToolButton* makeChromeButton(const QString& iconPath);
|
QToolButton* makeChromeButton(const QString& iconPath);
|
||||||
void toggleMaximize();
|
void toggleMaximize();
|
||||||
|
|||||||
@@ -29,46 +29,88 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
|||||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||||
|
|
||||||
// Collect all top-level structs/enums across all tabs
|
// 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) {
|
for (const auto& tab : tabs) {
|
||||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||||
for (int idx : topLevel) {
|
for (int idx : topLevel) {
|
||||||
const Node& n = tab.tree->nodes[idx];
|
const Node& n = tab.tree->nodes[idx];
|
||||||
if (n.kind != NodeKind::Struct) continue;
|
if (n.kind != NodeKind::Struct) continue;
|
||||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||||
enums.append({&n, tab.subPtr});
|
enums.append({&n, tab.subPtr, tab.tree});
|
||||||
else
|
else
|
||||||
types.append({&n, tab.subPtr});
|
types.append({&n, tab.subPtr, tab.tree});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto nameOf = [](const Node* n) {
|
auto nameOf = [](const Node* n) {
|
||||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||||
};
|
};
|
||||||
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||||
const std::pair<const Node*, void*>& b) {
|
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||||
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
|
||||||
};
|
};
|
||||||
std::sort(types.begin(), types.end(), cmpName);
|
std::sort(types.begin(), types.end(), cmpName);
|
||||||
std::sort(enums.begin(), enums.end(), cmpName);
|
std::sort(enums.begin(), enums.end(), cmpName);
|
||||||
|
|
||||||
for (const auto& [n, subPtr] : types) {
|
// Helper: type display string for a member node
|
||||||
QString display = QStringLiteral("%1 (%2)")
|
auto memberTypeName = [](const Node& m) -> QString {
|
||||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
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(
|
auto* item = new QStandardItem(
|
||||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
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);
|
projectItem->appendRow(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& [n, subPtr] : enums) {
|
for (const auto& e : enums) {
|
||||||
QString display = QStringLiteral("%1 (%2)")
|
int count = e.node->enumMembers.size();
|
||||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||||
|
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||||
|
QString::number(count));
|
||||||
auto* item = new QStandardItem(
|
auto* item = new QStandardItem(
|
||||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||||
projectItem->appendRow(item);
|
projectItem->appendRow(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) {
|
|||||||
QString src = buildSourceLabel(prov);
|
QString src = buildSourceLabel(prov);
|
||||||
QString addr = QStringLiteral("0x") +
|
QString addr = QStringLiteral("0x") +
|
||||||
QString::number(baseAddress, 16).toUpper();
|
QString::number(baseAddress, 16).toUpper();
|
||||||
return QStringLiteral(" %1 \u00B7 %2").arg(src, addr);
|
return QStringLiteral(" %1 %2").arg(src, addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Replicate commandRowSrcSpan for testing
|
// -- Replicate commandRowSrcSpan for testing
|
||||||
@@ -32,17 +32,13 @@ struct TestColumnSpan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
|
static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||||
if (idx < 0) return {};
|
if (arrow < 0) return {};
|
||||||
int start = 0;
|
int start = 0;
|
||||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||||
if (start >= idx) return {};
|
if (start >= arrow) return {};
|
||||||
// Exclude trailing ▾ from the editable span
|
return {start, arrow, true};
|
||||||
int end = idx;
|
|
||||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
|
||||||
if (end <= start) return {};
|
|
||||||
return {start, end, true};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCommandRow : public QObject {
|
class TestCommandRow : public QObject {
|
||||||
@@ -77,13 +73,13 @@ private slots:
|
|||||||
void row_nullProvider() {
|
void row_nullProvider() {
|
||||||
NullProvider p;
|
NullProvider p;
|
||||||
QString row = buildCommandRow(p, 0);
|
QString row = buildCommandRow(p, 0);
|
||||||
QCOMPARE(row, QStringLiteral(" source\u25BE \u00B7 0x0"));
|
QCOMPARE(row, QStringLiteral(" source\u25BE 0x0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void row_fileProvider() {
|
void row_fileProvider() {
|
||||||
BufferProvider p(QByteArray(4, '\0'), "test.bin");
|
BufferProvider p(QByteArray(4, '\0'), "test.bin");
|
||||||
QString row = buildCommandRow(p, 0x140000000ULL);
|
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() {
|
void span_processProvider_simulated() {
|
||||||
// Simulate a process provider without needing Windows APIs
|
// Simulate a process provider without needing Windows APIs
|
||||||
// by building the string directly
|
// by building the string directly
|
||||||
QString row = QStringLiteral(" 'notepad.exe'\u25BE \u00B7 0x7FF600000000");
|
QString row = QStringLiteral(" 'notepad.exe'\u25BE 0x7FF600000000");
|
||||||
auto span = commandRowSrcSpan(row);
|
auto span = commandRowSrcSpan(row);
|
||||||
QVERIFY(span.valid);
|
QVERIFY(span.valid);
|
||||||
QString extracted = row.mid(span.start, span.end - span.start);
|
QString extracted = row.mid(span.start, span.end - span.start);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include <QtTest/QTest>
|
#include <QtTest/QTest>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QFile>
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
|
||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
@@ -1922,7 +1924,7 @@ private slots:
|
|||||||
|
|
||||||
void testCommandRowRootNameSpan() {
|
void testCommandRowRootNameSpan() {
|
||||||
// Name span should cover the class name in the merged command row
|
// 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);
|
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||||
QVERIFY(nameSpan.valid);
|
QVERIFY(nameSpan.valid);
|
||||||
|
|
||||||
@@ -1984,6 +1986,455 @@ private slots:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// Union tests
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void testUnionHeaderShowsKeyword() {
|
||||||
|
// Union (Struct with classKeyword="union") should display "union" in header
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "Root";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// Union container
|
||||||
|
Node u;
|
||||||
|
u.kind = NodeKind::Struct;
|
||||||
|
u.classKeyword = "union";
|
||||||
|
u.name = "u1";
|
||||||
|
u.parentId = rootId;
|
||||||
|
u.offset = 0;
|
||||||
|
int ui = tree.addNode(u);
|
||||||
|
uint64_t uId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
// Two members at offset 0
|
||||||
|
Node m1;
|
||||||
|
m1.kind = NodeKind::UInt32;
|
||||||
|
m1.name = "asInt";
|
||||||
|
m1.parentId = uId;
|
||||||
|
m1.offset = 0;
|
||||||
|
tree.addNode(m1);
|
||||||
|
|
||||||
|
Node m2;
|
||||||
|
m2.kind = NodeKind::Float;
|
||||||
|
m2.name = "asFloat";
|
||||||
|
m2.parentId = uId;
|
||||||
|
m2.offset = 0;
|
||||||
|
tree.addNode(m2);
|
||||||
|
|
||||||
|
NullProvider prov;
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
QStringList lines = result.text.split('\n');
|
||||||
|
|
||||||
|
// Find the union header line
|
||||||
|
int headerLine = -1;
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
if (result.meta[i].lineKind == LineKind::Header &&
|
||||||
|
result.meta[i].nodeKind == NodeKind::Struct &&
|
||||||
|
result.meta[i].depth == 1) {
|
||||||
|
headerLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(headerLine >= 0);
|
||||||
|
QVERIFY2(lines[headerLine].contains("union"),
|
||||||
|
qPrintable("Union header should contain 'union': " + lines[headerLine]));
|
||||||
|
|
||||||
|
// Both members should be rendered at depth 2
|
||||||
|
int memberCount = 0;
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||||
|
memberCount++;
|
||||||
|
}
|
||||||
|
QCOMPARE(memberCount, 2);
|
||||||
|
|
||||||
|
// Both members share the same offset text (both at 0000)
|
||||||
|
QVector<int> memberLines;
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||||
|
memberLines.append(i);
|
||||||
|
}
|
||||||
|
QCOMPARE(memberLines.size(), 2);
|
||||||
|
QCOMPARE(result.meta[memberLines[0]].offsetText,
|
||||||
|
result.meta[memberLines[1]].offsetText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testUnionCollapsed() {
|
||||||
|
// Collapsed union should hide children
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "Root";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
Node u;
|
||||||
|
u.kind = NodeKind::Struct;
|
||||||
|
u.classKeyword = "union";
|
||||||
|
u.name = "u1";
|
||||||
|
u.parentId = rootId;
|
||||||
|
u.offset = 0;
|
||||||
|
u.collapsed = true;
|
||||||
|
int ui = tree.addNode(u);
|
||||||
|
uint64_t uId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
Node m;
|
||||||
|
m.kind = NodeKind::UInt64;
|
||||||
|
m.name = "val";
|
||||||
|
m.parentId = uId;
|
||||||
|
m.offset = 0;
|
||||||
|
tree.addNode(m);
|
||||||
|
|
||||||
|
NullProvider prov;
|
||||||
|
ComposeResult result = compose(tree, prov);
|
||||||
|
|
||||||
|
// No field lines at depth 2
|
||||||
|
int deepFields = 0;
|
||||||
|
for (const auto& lm : result.meta) {
|
||||||
|
if (lm.lineKind == LineKind::Field && lm.depth >= 2)
|
||||||
|
deepFields++;
|
||||||
|
}
|
||||||
|
QCOMPARE(deepFields, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testUnionStructSpan() {
|
||||||
|
// structSpan of a union = max(child offset + size), not sum
|
||||||
|
NodeTree tree;
|
||||||
|
|
||||||
|
Node u;
|
||||||
|
u.kind = NodeKind::Struct;
|
||||||
|
u.classKeyword = "union";
|
||||||
|
u.name = "U";
|
||||||
|
u.parentId = 0;
|
||||||
|
u.offset = 0;
|
||||||
|
int ui = tree.addNode(u);
|
||||||
|
uint64_t uId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
// 2-byte member
|
||||||
|
Node m1;
|
||||||
|
m1.kind = NodeKind::UInt16;
|
||||||
|
m1.name = "small";
|
||||||
|
m1.parentId = uId;
|
||||||
|
m1.offset = 0;
|
||||||
|
tree.addNode(m1);
|
||||||
|
|
||||||
|
// 8-byte member
|
||||||
|
Node m2;
|
||||||
|
m2.kind = NodeKind::UInt64;
|
||||||
|
m2.name = "big";
|
||||||
|
m2.parentId = uId;
|
||||||
|
m2.offset = 0;
|
||||||
|
tree.addNode(m2);
|
||||||
|
|
||||||
|
// structSpan = max(0+2, 0+8) = 8
|
||||||
|
QCOMPARE(tree.structSpan(uId), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// Enum compose tests
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void testEnumDisplaysMembers() {
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "Root";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
Node e;
|
||||||
|
e.kind = NodeKind::Struct;
|
||||||
|
e.classKeyword = "enum";
|
||||||
|
e.name = "Color";
|
||||||
|
e.structTypeName = "Color";
|
||||||
|
e.parentId = rootId;
|
||||||
|
e.offset = 0;
|
||||||
|
e.collapsed = false;
|
||||||
|
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
|
||||||
|
tree.addNode(e);
|
||||||
|
|
||||||
|
NullProvider prov;
|
||||||
|
auto result = compose(tree, prov);
|
||||||
|
|
||||||
|
// Should have enum members in the text
|
||||||
|
QVERIFY(result.text.contains("Red"));
|
||||||
|
QVERIFY(result.text.contains("Green"));
|
||||||
|
QVERIFY(result.text.contains("Blue"));
|
||||||
|
QVERIFY(result.text.contains("= 0"));
|
||||||
|
QVERIFY(result.text.contains("= 2"));
|
||||||
|
// Header should contain the type name
|
||||||
|
QVERIFY(result.text.contains("Color"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testEnumCollapsed() {
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0;
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
root.kind = NodeKind::Struct;
|
||||||
|
root.name = "Root";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
Node e;
|
||||||
|
e.kind = NodeKind::Struct;
|
||||||
|
e.classKeyword = "enum";
|
||||||
|
e.name = "Flags";
|
||||||
|
e.structTypeName = "Flags";
|
||||||
|
e.parentId = rootId;
|
||||||
|
e.offset = 0;
|
||||||
|
e.collapsed = true;
|
||||||
|
e.enumMembers = {{"A", 0}, {"B", 1}};
|
||||||
|
tree.addNode(e);
|
||||||
|
|
||||||
|
NullProvider prov;
|
||||||
|
auto result = compose(tree, prov);
|
||||||
|
|
||||||
|
// Collapsed: members should NOT appear
|
||||||
|
QVERIFY(!result.text.contains("= 0"));
|
||||||
|
QVERIFY(!result.text.contains("= 1"));
|
||||||
|
// But header should still show the type name
|
||||||
|
QVERIFY(result.text.contains("Flags"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// Compact columns: load EPROCESS.rcx and compare output
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void testCompactColumnsEprocess() {
|
||||||
|
// Load the EPROCESS example .rcx
|
||||||
|
// Try multiple paths: build dir examples, or source dir
|
||||||
|
QString rcxPath;
|
||||||
|
QStringList candidates = {
|
||||||
|
QCoreApplication::applicationDirPath() + "/examples/EPROCESS.rcx",
|
||||||
|
QCoreApplication::applicationDirPath() + "/../src/examples/EPROCESS.rcx",
|
||||||
|
};
|
||||||
|
for (const auto& c : candidates) {
|
||||||
|
if (QFile::exists(c)) { rcxPath = c; break; }
|
||||||
|
}
|
||||||
|
if (rcxPath.isEmpty())
|
||||||
|
QSKIP("EPROCESS.rcx not found");
|
||||||
|
QFile file(rcxPath);
|
||||||
|
QVERIFY2(file.open(QIODevice::ReadOnly),
|
||||||
|
qPrintable("Cannot open " + rcxPath));
|
||||||
|
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
|
||||||
|
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||||
|
NullProvider prov;
|
||||||
|
|
||||||
|
// Compose WITHOUT compact (default)
|
||||||
|
ComposeResult normal = compose(tree, prov, 0, false);
|
||||||
|
// Compose WITH compact
|
||||||
|
ComposeResult compact = compose(tree, prov, 0, true);
|
||||||
|
|
||||||
|
// Compact typeW should be capped at kCompactTypeW (22)
|
||||||
|
QVERIFY2(compact.layout.typeW <= kCompactTypeW,
|
||||||
|
qPrintable(QString("compact typeW=%1, expected <= %2")
|
||||||
|
.arg(compact.layout.typeW).arg(kCompactTypeW)));
|
||||||
|
|
||||||
|
// Normal typeW should be wider (the _EPROCESS has long type names)
|
||||||
|
QVERIFY2(normal.layout.typeW > compact.layout.typeW,
|
||||||
|
qPrintable(QString("normal typeW=%1 should exceed compact typeW=%2")
|
||||||
|
.arg(normal.layout.typeW).arg(compact.layout.typeW)));
|
||||||
|
|
||||||
|
// Print side-by-side sample for visual inspection
|
||||||
|
QStringList normalLines = normal.text.split('\n');
|
||||||
|
QStringList compactLines = compact.text.split('\n');
|
||||||
|
qDebug() << "\n=== EPROCESS compact columns comparison ===";
|
||||||
|
qDebug() << "Normal typeW:" << normal.layout.typeW
|
||||||
|
<< " Compact typeW:" << compact.layout.typeW;
|
||||||
|
qDebug() << "Normal lines:" << normalLines.size()
|
||||||
|
<< " Compact lines:" << compactLines.size();
|
||||||
|
|
||||||
|
// Dump full output to files for visual diffing
|
||||||
|
{
|
||||||
|
QFile nf(QCoreApplication::applicationDirPath() + "/../eprocess_normal.txt");
|
||||||
|
nf.open(QIODevice::WriteOnly);
|
||||||
|
nf.write(normal.text.toUtf8());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QFile cf(QCoreApplication::applicationDirPath() + "/../eprocess_compact.txt");
|
||||||
|
cf.open(QIODevice::WriteOnly);
|
||||||
|
cf.write(compact.text.toUtf8());
|
||||||
|
}
|
||||||
|
qDebug() << "Wrote eprocess_normal.txt and eprocess_compact.txt";
|
||||||
|
|
||||||
|
// Show first 50 lines of each for quick inspection
|
||||||
|
qDebug() << "\n--- NORMAL (first 50 lines) ---";
|
||||||
|
for (int i = 0; i < qMin(50, normalLines.size()); ++i)
|
||||||
|
qDebug().noquote() << normalLines[i];
|
||||||
|
|
||||||
|
qDebug() << "\n--- COMPACT (first 50 lines) ---";
|
||||||
|
for (int i = 0; i < qMin(50, compactLines.size()); ++i)
|
||||||
|
qDebug().noquote() << compactLines[i];
|
||||||
|
|
||||||
|
// Overflow types should print in full (no truncation)
|
||||||
|
bool foundFull = false;
|
||||||
|
for (const QString& l : compactLines) {
|
||||||
|
if (l.contains("_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES")) {
|
||||||
|
foundFull = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(foundFull,
|
||||||
|
"Long type _PS_DYNAMIC_ENFORCED_ADDRESS_RANGES should print in full (no truncation)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMmpfnRcxLoadsAndComposes() {
|
||||||
|
// Load the MMPFN.rcx example file and verify it composes without errors
|
||||||
|
// Try several paths to find the .rcx file
|
||||||
|
QString rcxPath;
|
||||||
|
for (const auto& p : {
|
||||||
|
QStringLiteral("../src/examples/MMPFN.rcx"),
|
||||||
|
QStringLiteral("../../src/examples/MMPFN.rcx"),
|
||||||
|
QStringLiteral("src/examples/MMPFN.rcx")}) {
|
||||||
|
if (QFile::exists(p)) { rcxPath = p; break; }
|
||||||
|
}
|
||||||
|
if (rcxPath.isEmpty()) {
|
||||||
|
QSKIP("MMPFN.rcx not found (run from build dir)");
|
||||||
|
}
|
||||||
|
QFile f(rcxPath);
|
||||||
|
QVERIFY2(f.open(QIODevice::ReadOnly), "Cannot open MMPFN.rcx");
|
||||||
|
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
QVERIFY(jdoc.isObject());
|
||||||
|
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||||
|
|
||||||
|
QVERIFY2(tree.nodes.size() >= 60, "Expected at least 60 nodes");
|
||||||
|
|
||||||
|
// Check key top-level types exist
|
||||||
|
bool hasMmpfn = false, hasListEntry = false, hasMmpte = false;
|
||||||
|
for (const auto& n : tree.nodes) {
|
||||||
|
if (n.parentId == 0 && n.structTypeName == "_MMPFN") hasMmpfn = true;
|
||||||
|
if (n.parentId == 0 && n.structTypeName == "_LIST_ENTRY") hasListEntry = true;
|
||||||
|
if (n.parentId == 0 && n.structTypeName == "_MMPTE") hasMmpte = true;
|
||||||
|
}
|
||||||
|
QVERIFY2(hasMmpfn, "Missing _MMPFN top-level type");
|
||||||
|
QVERIFY2(hasListEntry, "Missing _LIST_ENTRY top-level type");
|
||||||
|
QVERIFY2(hasMmpte, "Missing _MMPTE top-level type");
|
||||||
|
|
||||||
|
// Compose and verify output
|
||||||
|
NullProvider prov;
|
||||||
|
ComposeResult result = compose(tree, prov, 0, false);
|
||||||
|
QStringList lines = result.text.split('\n');
|
||||||
|
QVERIFY2(lines.size() > 10, "Expected non-trivial compose output");
|
||||||
|
|
||||||
|
// Print first 30 lines for manual inspection
|
||||||
|
qDebug() << "=== MMPFN compose output ===";
|
||||||
|
for (int i = 0; i < qMin(30, lines.size()); ++i)
|
||||||
|
qDebug().noquote() << lines[i];
|
||||||
|
qDebug() << "... total lines:" << lines.size();
|
||||||
|
|
||||||
|
// Verify _MMPFN header appears in output
|
||||||
|
bool foundMmpfn = false;
|
||||||
|
for (const auto& l : lines) {
|
||||||
|
if (l.contains("_MMPFN")) { foundMmpfn = true; break; }
|
||||||
|
}
|
||||||
|
QVERIFY2(foundMmpfn, "Compose output should contain _MMPFN");
|
||||||
|
|
||||||
|
// Verify no M_CYCLE markers on any lines (all self-ref pointers are collapsed)
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
bool hasCycle = (result.meta[i].markerMask & (1u << M_CYCLE)) != 0;
|
||||||
|
QVERIFY2(!hasCycle,
|
||||||
|
qPrintable(QString("Unexpected cycle marker on line %1").arg(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
QTEST_MAIN(TestCompose)
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ private slots:
|
|||||||
|
|
||||||
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
|
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
|
||||||
m_editor->setCommandRowText(
|
m_editor->setCommandRowText(
|
||||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||||
|
|
||||||
// BaseAddress should be ALLOWED on CommandRow (ADDR field)
|
// BaseAddress should be ALLOWED on CommandRow (ADDR field)
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||||
@@ -816,7 +816,7 @@ private slots:
|
|||||||
|
|
||||||
// Set CommandRow text with ADDR value (simulates controller)
|
// Set CommandRow text with ADDR value (simulates controller)
|
||||||
m_editor->setCommandRowText(
|
m_editor->setCommandRowText(
|
||||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||||
|
|
||||||
// Line 0 is CommandRow
|
// Line 0 is CommandRow
|
||||||
const LineMeta* lm = m_editor->metaForLine(0);
|
const LineMeta* lm = m_editor->metaForLine(0);
|
||||||
@@ -901,7 +901,7 @@ private slots:
|
|||||||
|
|
||||||
// Set CommandRow text with ADDR value (simulates controller)
|
// Set CommandRow text with ADDR value (simulates controller)
|
||||||
m_editor->setCommandRowText(
|
m_editor->setCommandRowText(
|
||||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000"));
|
QStringLiteral("source\u25BE 0xD87B5E5000"));
|
||||||
|
|
||||||
// Begin base address edit on line 0 (CommandRow ADDR field)
|
// Begin base address edit on line 0 (CommandRow ADDR field)
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
|
||||||
@@ -1038,7 +1038,7 @@ private slots:
|
|||||||
|
|
||||||
// Set CommandRow text with root class (simulates controller.updateCommandRow)
|
// Set CommandRow text with root class (simulates controller.updateCommandRow)
|
||||||
m_editor->setCommandRowText(
|
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)
|
// RootClassName should be allowed on CommandRow (line 0)
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
|
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
|
||||||
@@ -1053,7 +1053,7 @@ private slots:
|
|||||||
|
|
||||||
// Set CommandRow with root class
|
// Set CommandRow with root class
|
||||||
m_editor->setCommandRowText(
|
m_editor->setCommandRowText(
|
||||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {"));
|
||||||
|
|
||||||
// Line 0 is CommandRow
|
// Line 0 is CommandRow
|
||||||
const LineMeta* lm = m_editor->metaForLine(0);
|
const LineMeta* lm = m_editor->metaForLine(0);
|
||||||
@@ -1099,7 +1099,7 @@ private slots:
|
|||||||
|
|
||||||
// Set command row text (simulates controller.updateCommandRow)
|
// Set command row text (simulates controller.updateCommandRow)
|
||||||
QString cmdText = QStringLiteral(
|
QString cmdText = QStringLiteral(
|
||||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||||
m_editor->setCommandRowText(cmdText);
|
m_editor->setCommandRowText(cmdText);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
@@ -1177,7 +1177,7 @@ private slots:
|
|||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
QString cmdText = QStringLiteral(
|
QString cmdText = QStringLiteral(
|
||||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
"source\u25BE 0xD87B5E5000 struct _PEB64 {");
|
||||||
m_editor->setCommandRowText(cmdText);
|
m_editor->setCommandRowText(cmdText);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void testFmtPointer64_null() {
|
void testFmtPointer64_null() {
|
||||||
QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL"));
|
QCOMPARE(fmt::fmtPointer64(0), QString("0x0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void testFmtPointer64_nonNull() {
|
void testFmtPointer64_nonNull() {
|
||||||
QString s = fmt::fmtPointer64(0x400000);
|
QString s = fmt::fmtPointer64(0x400000);
|
||||||
QVERIFY(s.startsWith("-> 0x"));
|
QVERIFY(s.startsWith("0x"));
|
||||||
QVERIFY(s.contains("400000"));
|
QVERIFY(s.contains("400000"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ private slots:
|
|||||||
void forwardDeclaration();
|
void forwardDeclaration();
|
||||||
|
|
||||||
// Union handling
|
// Union handling
|
||||||
void unionPickFirst();
|
void unionContainer();
|
||||||
|
void unionWithCommentOffsets();
|
||||||
|
void namedUnion();
|
||||||
|
|
||||||
// Padding fields
|
// Padding fields
|
||||||
void paddingFieldExpansion();
|
void paddingFieldExpansion();
|
||||||
@@ -69,11 +71,19 @@ private slots:
|
|||||||
|
|
||||||
// Edge cases
|
// Edge cases
|
||||||
void bitfieldSkipped();
|
void bitfieldSkipped();
|
||||||
|
void bitfieldWithOffsetsEmitsHex();
|
||||||
void hexArraySizes();
|
void hexArraySizes();
|
||||||
void windowsStylePEB();
|
void windowsStylePEB();
|
||||||
void classKeyword();
|
void classKeyword();
|
||||||
void inheritanceSkipped();
|
void inheritanceSkipped();
|
||||||
|
|
||||||
|
// Enum tests
|
||||||
|
void enumBasic();
|
||||||
|
void enumAutoValues();
|
||||||
|
void enumHexValues();
|
||||||
|
void enumInStruct();
|
||||||
|
void enumClass();
|
||||||
|
|
||||||
// Round-trip test (requires generator.h)
|
// Round-trip test (requires generator.h)
|
||||||
void basicRoundTrip();
|
void basicRoundTrip();
|
||||||
};
|
};
|
||||||
@@ -575,7 +585,7 @@ void TestImportSource::forwardDeclaration() {
|
|||||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestImportSource::unionPickFirst() {
|
void TestImportSource::unionContainer() {
|
||||||
NodeTree tree = importFromSource(QStringLiteral(
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
"struct WithUnion {\n"
|
"struct WithUnion {\n"
|
||||||
" union {\n"
|
" union {\n"
|
||||||
@@ -586,12 +596,85 @@ void TestImportSource::unionPickFirst() {
|
|||||||
"};\n"
|
"};\n"
|
||||||
));
|
));
|
||||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
// Should have 2 fields: asFloat (first union member) + after
|
// Should have 2 direct children: union container + after
|
||||||
QCOMPARE(kids.size(), 2);
|
QCOMPARE(kids.size(), 2);
|
||||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
|
||||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
// First child is the union container
|
||||||
|
const auto& unionNode = tree.nodes[kids[0]];
|
||||||
|
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||||
|
QCOMPARE(unionNode.offset, 0);
|
||||||
|
|
||||||
|
// Union has 2 children, both at offset 0
|
||||||
|
auto unionKids = childrenOf(tree, unionNode.id);
|
||||||
|
QCOMPARE(unionKids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[0]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[0]].name, QStringLiteral("asFloat"));
|
||||||
|
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[1]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[1]].name, QStringLiteral("asInt"));
|
||||||
|
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||||
|
|
||||||
|
// structSpan of union = max member size = 4
|
||||||
|
QCOMPARE(tree.structSpan(unionNode.id), 4);
|
||||||
|
|
||||||
|
// after field follows the union at offset 4
|
||||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::unionWithCommentOffsets() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct S {\n"
|
||||||
|
" uint64_t a; // 0x0\n"
|
||||||
|
" union {\n"
|
||||||
|
" uint32_t x; // 0x8\n"
|
||||||
|
" float y; // 0x8\n"
|
||||||
|
" };\n"
|
||||||
|
" uint32_t b; // 0xC\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 3); // a + union + b
|
||||||
|
|
||||||
|
// Union at offset 0x8
|
||||||
|
const auto& unionNode = tree.nodes[kids[1]];
|
||||||
|
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||||
|
QCOMPARE(unionNode.offset, 0x8);
|
||||||
|
|
||||||
|
// Union members at offset 0 (relative to union)
|
||||||
|
auto unionKids = childrenOf(tree, unionNode.id);
|
||||||
|
QCOMPARE(unionKids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||||
|
|
||||||
|
// b at 0xC
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::namedUnion() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct S {\n"
|
||||||
|
" union {\n"
|
||||||
|
" uint16_t shortVal;\n"
|
||||||
|
" uint64_t longVal;\n"
|
||||||
|
" } u3;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
|
||||||
|
const auto& unionNode = tree.nodes[kids[0]];
|
||||||
|
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||||
|
QCOMPARE(unionNode.name, QStringLiteral("u3"));
|
||||||
|
|
||||||
|
auto unionKids = childrenOf(tree, unionNode.id);
|
||||||
|
QCOMPARE(unionKids.size(), 2);
|
||||||
|
// structSpan = max(2, 8) = 8
|
||||||
|
QCOMPARE(tree.structSpan(unionNode.id), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestImportSource::paddingFieldExpansion() {
|
void TestImportSource::paddingFieldExpansion() {
|
||||||
@@ -697,6 +780,7 @@ void TestImportSource::structPrefixOnType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TestImportSource::bitfieldSkipped() {
|
void TestImportSource::bitfieldSkipped() {
|
||||||
|
// Bitfields emit a bitfield container with named members
|
||||||
NodeTree tree = importFromSource(QStringLiteral(
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
"struct BF {\n"
|
"struct BF {\n"
|
||||||
" uint32_t normal;\n"
|
" uint32_t normal;\n"
|
||||||
@@ -706,10 +790,55 @@ void TestImportSource::bitfieldSkipped() {
|
|||||||
"};\n"
|
"};\n"
|
||||||
));
|
));
|
||||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
// Bitfields should be skipped, only normal + after
|
// normal + bitfield container (16 bits → 2 bytes) + after
|
||||||
QCOMPARE(kids.size(), 2);
|
QCOMPARE(kids.size(), 3);
|
||||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct BF2 {\n"
|
||||||
|
" uint32_t normal; // 0x0\n"
|
||||||
|
" ULONGLONG Valid : 1; // 0x4\n"
|
||||||
|
" ULONGLONG Dirty : 1; // 0x4\n"
|
||||||
|
" ULONGLONG PageFrameNumber : 36; // 0x4\n"
|
||||||
|
" ULONGLONG Reserved : 26; // 0x4\n"
|
||||||
|
" uint32_t after; // 0xC\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// 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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestImportSource::hexArraySizes() {
|
void TestImportSource::hexArraySizes() {
|
||||||
@@ -842,5 +971,78 @@ void TestImportSource::basicRoundTrip() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Enum tests ──
|
||||||
|
|
||||||
|
void TestImportSource::enumBasic() {
|
||||||
|
auto tree = importFromSource(QStringLiteral(
|
||||||
|
"enum Color { Red = 0, Green = 1, Blue = 2 };"));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||||
|
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Color"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("Red"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[1].first, QStringLiteral("Green"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[2].first, QStringLiteral("Blue"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::enumAutoValues() {
|
||||||
|
auto tree = importFromSource(QStringLiteral(
|
||||||
|
"enum Flags { A, B, C };"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::enumHexValues() {
|
||||||
|
auto tree = importFromSource(QStringLiteral(
|
||||||
|
"enum { X = 0x10, Y = 0x20 };"));
|
||||||
|
// Anonymous enum has no name — parser skips it (unnamed enums are not added)
|
||||||
|
// Actually, let's use a named enum with hex values
|
||||||
|
tree = importFromSource(QStringLiteral(
|
||||||
|
"enum Hex { X = 0x10, Y = 0x20 };"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0x10LL);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[1].second, 0x20LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::enumInStruct() {
|
||||||
|
auto tree = importFromSource(QStringLiteral(
|
||||||
|
"enum PoolType { NonPaged = 0, Paged = 1 };\n"
|
||||||
|
"struct Foo {\n"
|
||||||
|
" PoolType pool; //0x0\n"
|
||||||
|
" uint32_t size; //0x4\n"
|
||||||
|
"};"));
|
||||||
|
// Should have 2 roots: PoolType enum + Foo struct
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
|
||||||
|
// Find Foo struct
|
||||||
|
int fooIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Foo")) { fooIdx = i; break; }
|
||||||
|
}
|
||||||
|
QVERIFY(fooIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
// First child should be UInt32 (enum mapped to int) with refId to PoolType
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("pool"));
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0); // linked to enum definition
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::enumClass() {
|
||||||
|
auto tree = importFromSource(QStringLiteral(
|
||||||
|
"enum class Scope : uint8_t { A = 1, B = 2 };"));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||||
|
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Scope"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("A"));
|
||||||
|
QCOMPARE(tree.nodes[0].enumMembers[0].second, 1LL);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_MAIN(TestImportSource)
|
QTEST_MAIN(TestImportSource)
|
||||||
#include "test_import_source.moc"
|
#include "test_import_source.moc"
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ private slots:
|
|||||||
// ── Chevron span detection ──
|
// ── Chevron span detection ──
|
||||||
|
|
||||||
void testChevronSpanDetected() {
|
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);
|
ColumnSpan span = commandRowChevronSpan(text);
|
||||||
QVERIFY(span.valid);
|
QVERIFY(span.valid);
|
||||||
QCOMPARE(span.start, 0);
|
QCOMPARE(span.start, 0);
|
||||||
@@ -80,7 +80,7 @@ private slots:
|
|||||||
// ── Existing spans unbroken by chevron prefix ──
|
// ── Existing spans unbroken by chevron prefix ──
|
||||||
|
|
||||||
void testSpansWithPrefix() {
|
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);
|
ColumnSpan src = commandRowSrcSpan(text);
|
||||||
QVERIFY(src.valid);
|
QVERIFY(src.valid);
|
||||||
|
|||||||
Reference in New Issue
Block a user