Compare commits

...

16 Commits

Author SHA1 Message Date
IChooseYou
5fa1dd0ab4 fix: add missing header declarations and editor scroll fixes
- mainwindow.h: add m_viewBtnGroup, m_btnReclass, m_btnRendered members,
  syncViewButtons() declaration, QButtonGroup/QPushButton includes,
  remove applyTabWidgetStyle() declaration
- editor.cpp: reset xOffset on applyDocument, clamp in restoreViewState
- test_editor.cpp: add horizontal scroll reset test
2026-02-20 13:22:23 -07:00
IChooseYou
3b1fe7ff35 fix: use findChild<QWidget*> for ResizeGrip to fix GCC 15 static_assert
GCC 15.2 on CI enforces Q_OBJECT requirement for findChild template
parameter. ResizeGrip is a local class without Q_OBJECT, so use
QWidget* with static_cast instead.
2026-02-20 13:18:03 -07:00
IChooseYou
4595b366e3 ci: use system MinGW from runner, drop tools_mingw1310 2026-02-20 13:05:13 -07:00
IChooseYou
33d7dc74cb ci: switch Windows CI from MSVC to MinGW, run Linux in parallel 2026-02-20 12:57:51 -07:00
IChooseYou
e118231bb1 docs: add screenshots to README 2026-02-20 12:32:04 -07:00
IChooseYou
0cfd7ad87a feat: sort primitives alphabetically in type chooser 2026-02-20 07:37:32 -07:00
IChooseYou
2d3ce63b54 ci: disable UI tests in CI, delete test_com_security
CI now passes -DBUILD_UI_TESTS=OFF so only headless tests
(core, format, compose, provider, command_row, generator,
import_xml, import_source, export_xml, disasm) build and run.

Removed xvfb-run and exclude-regex hacks from both Windows
and Linux CI — the CMake option handles it cleanly.

Deleted test_com_security (windbg-only, not needed in CI).
2026-02-20 07:27:23 -07:00
IChooseYou
0e087fa3a4 feat: primitive pointer modifiers, type chooser fixes, double-click to edit
Type chooser:
- Fix PointerTarget mode hiding primitives due to stale modifier state
- Preselect */[n] modifier buttons to reflect current node type
- Primitive pointer support: int32*, double**, etc with provider deref
- hex64*/ptr64* with * modifier falls back to void* (meaningless deref)
- isValidPrimitivePtrTarget guard in controller, compose, format
- Modifier toggle no longer resets list selection
- Primitive pointers open FieldType mode (not PointerTarget)
- Type edit requires double-click (was single-click, too easy to misclick)

Other:
- Custom dock titlebar with themed close button, no float button
- Status bar font synced at startup
- Resize grip reworked as direct MainWindow child, font-independent
- File menu "Source" renamed to "Current Tab Source"

Tests: 41 type_selector, 39 editor, 17 controller (200 total, 0 failures)
2026-02-20 07:21:02 -07:00
IChooseYou
c7afe363f3 feat: custom dock titlebar, resize grip symmetry fix, status bar font sync
- Replace default dock widget titlebar with custom label + themed ✕ close button
- Remove float/popout button from project tree dock
- Fix resize grip corner symmetry (bottom margin 4→0)
- Sync editor font to status bar and dock titlebar at startup
- Add testResizeGripCornerSymmetry test
2026-02-19 18:10:52 -07:00
IChooseYou
2a44d2ac57 fix: narrow inline editor selection for pointer values, resolve correct write address
resolvedSpanFor() now applies narrowPtrValueSpan() to trim the "// Module+offset"
symbol comment from the editable span, matching hitTestTarget(). Previously the
full value column text was selected, making the parser fail on commit (toULongLong
rejected the non-hex suffix), so pointer value saves were silently no-ops.

With the parse now succeeding, a second bug was exposed: setNodeValue() computed
write addresses via computeOffset() which sums tree offsets without dereferencing
pointers. For nodes inside expanded pointer targets (e.g. VTable entries), this
wrote to struct_base+child_offset instead of *ptr_value+child_offset, causing an
access violation crash. The fix passes lm->offsetAddr (the compose-resolved
absolute address) through the inlineEditCommitted signal so setNodeValue() uses
the correct dereferenced address.
2026-02-19 13:05:25 -07:00
IChooseYou
d989e2a947 feat: safe workspace tree deletion with reference cleanup and confirmation
- Add deleteRootStruct() that clears orphaned refId references before removal
- Show confirmation dialog listing all fields that reference the deleted type
- Auto-switch view to next root struct when the viewed one is deleted
- Entire operation is a single undo macro (Ctrl+Z restores everything)
2026-02-19 10:06:13 -07:00
IChooseYou
7678da033d feat: source management, cross-tab type visibility, default VS2022 theme
- Add clearSources() and File→Source submenu for provider management
- Fix type picker not showing newly created structs (empty structTypeName)
- Add cross-tab type visibility via shared project document list
- Import external types into local document on selection
- Default theme to VS2022 on first launch
- Add test_source_management and test_type_visibility test suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:29:18 -07:00
IChooseYou
acc3ebf5db feat: track value changes toggle, hover scroll fix, ptr* convert, hex split 2026-02-19 06:32:58 -07:00
IChooseYou
26217f5de8 feat: switch provider addressing from RVA to absolute, add pointer expansion tests 2026-02-18 13:07:48 -07:00
IChooseYou
fa0d9a377b fix: type chooser updates colors when theme changes
Add applyTheme() to TypeSelectorPopup that refreshes palette and
stylesheets for all child widgets. Controller connects it to
ThemeManager::themeChanged on popup creation.
2026-02-18 09:59:50 -07:00
IChooseYou
b1d3e52204 fix: type chooser SVG icons and gutter scale with editor zoom level
Derive icon size, gutter width, and icon column width from font
metrics instead of hardcoded 16/10/20 pixel values. Popup width
calculation also scales with font.
2026-02-18 09:47:25 -07:00
37 changed files with 3893 additions and 634 deletions

View File

@@ -22,22 +22,28 @@ jobs:
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
version: '6.8.1' version: '6.8.1'
arch: 'win64_msvc2022_64' arch: 'win64_mingw'
cache: true cache: true
aqtversion: '==3.1.21' aqtversion: '==3.1.21'
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Configure - name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release shell: bash
run: |
export PATH="/c/mingw64/bin:$PATH"
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF \
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
- name: Build - name: Build
run: cmake --build build shell: bash
run: |
export PATH="/c/mingw64/bin:$PATH"
cmake --build build
- name: Test - name: Test
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller|test_windbg_provider|test_com_security" shell: bash
run: |
export PATH="/c/mingw64/bin:$PATH"
ctest --test-dir build --output-on-failure
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -97,7 +103,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linux: linux:
needs: windows
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -118,15 +123,13 @@ jobs:
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0 sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
- name: Configure - name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
- name: Build - name: Build
run: cmake --build build run: cmake --build build
- name: Test - name: Test
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller" run: ctest --test-dir build --output-on-failure
env:
QT_QPA_PLATFORM: offscreen
- name: Create AppImage - name: Create AppImage
run: | run: |
@@ -188,4 +191,3 @@ jobs:
files: Reclass-linux64-qt6.AppImage files: Reclass-linux64-qt6.AppImage
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -256,6 +256,20 @@ if(BUILD_TESTING)
endif() endif()
add_test(NAME test_context_menu COMMAND test_context_menu) add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_source_management PRIVATE src third_party/fadec)
target_link_libraries(test_source_management PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_management COMMAND test_source_management)
add_executable(test_editor tests/test_editor.cpp add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/editor.cpp src/compose.cpp src/format.cpp
src/providerregistry.cpp src/providerregistry.cpp
@@ -302,6 +316,19 @@ if(BUILD_TESTING)
endif() endif()
add_test(NAME test_type_selector COMMAND test_type_selector) add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
target_link_libraries(test_type_visibility PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_visibility COMMAND test_type_visibility)
add_executable(test_options_dialog tests/test_options_dialog.cpp add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp) src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
@@ -318,14 +345,6 @@ if(BUILD_TESTING)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider) add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
endif() endif()
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
# Requires a running WinDbg debug server on port 5055
if(WIN32)
add_executable(test_com_security tests/test_com_security.cpp)
target_link_libraries(test_com_security PRIVATE dbgeng ole32 version)
add_test(NAME test_com_security COMMAND test_com_security)
endif()
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe # Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
# that links the broadest set of Qt modules; all test exes share the same output dir) # that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt) if(TARGET ${QT}::windeployqt)

View File

@@ -1,13 +1,25 @@
This tool 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 runtime or from some static source. This tool 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 runtime or from some static source.
## State ![Type chooser and struct inspection](docs/README_PIC1.png)
- MCP (Model Context Protocol) bridge via `ReclassMcpBridge.exe`. The server starts by default and can be stopped 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`): ![VTable pointer expansion with disassembly preview](docs/README_PIC2.png)
![Split view with rendered C/C++ output](docs/README_PIC3.png)
## Data Sources
- **File** — open any binary file and inspect its contents as structured data
- **Process** — attach to a live process and read its memory in real time
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
## 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`):
```json ```json
{ {
"mcpServers": { "mcpServers": {
"ReclassMcpBridge": { "ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge.exe", "command": "path/to/build/ReclassMcpBridge",
"args": [] "args": []
} }
} }

BIN
docs/README_PIC1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/README_PIC2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
docs/README_PIC3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -65,7 +65,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
if (!m_handle || len <= 0) return false; if (!m_handle || len <= 0) return false;
SIZE_T bytesRead = 0; SIZE_T bytesRead = 0;
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead); ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
if ((int)bytesRead < len) if ((int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead); memset((char*)buf + bytesRead, 0, len - bytesRead);
return bytesRead > 0; return bytesRead > 0;
@@ -76,7 +76,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
if (!m_handle || !m_writable || len <= 0) return false; if (!m_handle || !m_writable || len <= 0) return false;
SIZE_T bytesWritten = 0; SIZE_T bytesWritten = 0;
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten)) if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
return bytesWritten == (SIZE_T)len; return bytesWritten == (SIZE_T)len;
return false; return false;
} }
@@ -156,15 +156,13 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{ {
if (m_fd < 0 || len <= 0) return false; if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention) // Try process_vm_readv first (faster, no fd seek contention)
struct iovec local; struct iovec local;
local.iov_base = buf; local.iov_base = buf;
local.iov_len = static_cast<size_t>(len); local.iov_len = static_cast<size_t>(len);
struct iovec remote; struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr); remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len); remote.iov_len = static_cast<size_t>(len);
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0); ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
@@ -172,7 +170,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
return true; return true;
// Fallback: pread on /proc/<pid>/mem // Fallback: pread on /proc/<pid>/mem
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr)); nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nread == static_cast<ssize_t>(len); return nread == static_cast<ssize_t>(len);
} }
@@ -180,15 +178,13 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{ {
if (m_fd < 0 || !m_writable || len <= 0) return false; if (m_fd < 0 || !m_writable || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first // Try process_vm_writev first
struct iovec local; struct iovec local;
local.iov_base = const_cast<void*>(buf); local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len); local.iov_len = static_cast<size_t>(len);
struct iovec remote; struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr); remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len); remote.iov_len = static_cast<size_t>(len);
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0); ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
@@ -196,7 +192,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
return true; return true;
// Fallback: pwrite on /proc/<pid>/mem // Fallback: pwrite on /proc/<pid>/mem
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr)); nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nwritten == static_cast<ssize_t>(len); return nwritten == static_cast<ssize_t>(len);
} }

View File

@@ -27,11 +27,16 @@ public:
bool isLive() const override { return true; } bool isLive() const override { return true; }
uint64_t base() const override { return m_base; } uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; } bool isReadable(uint64_t, int len) const override {
#ifdef _WIN32
return m_handle && len >= 0;
#elif defined(__linux__)
return m_fd >= 0 && len >= 0;
#endif
}
// Process-specific helpers // Process-specific helpers
uint32_t pid() const { return m_pid; } uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); } void refreshModules() { m_modules.clear(); cacheModules(); }
private: private:

View File

@@ -33,9 +33,8 @@ bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0) if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
return false; return false;
uint64_t absAddr = m_base + addr;
return m_fns.ReadRemoteMemory(m_handle, return m_fns.ReadRemoteMemory(m_handle,
reinterpret_cast<RC_Pointer>(absAddr), reinterpret_cast<RC_Pointer>(addr),
static_cast<RC_Pointer>(buf), static_cast<RC_Pointer>(buf),
0, len); 0, len);
} }
@@ -54,9 +53,8 @@ bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0) if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
return false; return false;
uint64_t absAddr = m_base + addr;
return m_fns.WriteRemoteMemory(m_handle, return m_fns.WriteRemoteMemory(m_handle,
reinterpret_cast<RC_Pointer>(absAddr), reinterpret_cast<RC_Pointer>(addr),
const_cast<RC_Pointer>(static_cast<const void*>(buf)), const_cast<RC_Pointer>(static_cast<const void*>(buf)),
0, len); 0, len);
} }

View File

@@ -27,7 +27,6 @@ public:
QString kind() const override { return QStringLiteral("RcNet"); } QString kind() const override { return QStringLiteral("RcNet"); }
bool isLive() const override { return true; } bool isLive() const override { return true; }
uint64_t base() const override { return m_base; } uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
QString getSymbol(uint64_t addr) const override; QString getSymbol(uint64_t addr) const override;
struct ModuleInfo { struct ModuleInfo {

View File

@@ -304,7 +304,7 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
bool result = false; bool result = false;
dispatchToOwner([&]() { dispatchToOwner([&]() {
ULONG bytesRead = 0; ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead); HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (FAILED(hr) || (int)bytesRead < len) if (FAILED(hr) || (int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead); memset((char*)buf + bytesRead, 0, len - bytesRead);
result = bytesRead > 0; result = bytesRead > 0;
@@ -324,7 +324,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
bool result = false; bool result = false;
dispatchToOwner([&]() { dispatchToOwner([&]() {
ULONG bytesWritten = 0; ULONG bytesWritten = 0;
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf), HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
(ULONG)len, &bytesWritten); (ULONG)len, &bytesWritten);
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len; result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
}); });
@@ -364,7 +364,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
char nameBuf[512] = {}; char nameBuf[512] = {};
ULONG nameSize = 0; ULONG nameSize = 0;
ULONG64 displacement = 0; ULONG64 displacement = 0;
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf), HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
&nameSize, &displacement); &nameSize, &displacement);
if (SUCCEEDED(hr) && nameSize > 0) { if (SUCCEEDED(hr) && nameSize > 0) {
result = QString::fromUtf8(nameBuf); result = QString::fromUtf8(nameBuf);

View File

@@ -62,7 +62,6 @@ public:
bool isLive() const override { return m_isLive; } bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; } uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
private: private:
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client

View File

@@ -78,12 +78,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName; return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
} }
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
if (tree.baseAddress == 0) return ptr;
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
return UINT64_MAX; // Invalid: ptr below base address
}
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) { static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
int64_t total = 0; int64_t total = 0;
QSet<uint64_t> visited; QSet<uint64_t> visited;
@@ -125,9 +119,18 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString ptrTypeOverride; QString ptrTypeOverride;
QString ptrTargetName; QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) { if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
// Primitive pointer: e.g. "int32*" or "f64**"
const auto* meta = kindMeta(node.elementKind);
QString baseName = meta ? QString::fromLatin1(meta->typeName)
: QStringLiteral("void");
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
ptrTypeOverride = baseName + stars;
} else {
ptrTargetName = resolvePointerTarget(tree, node.refId); ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
} }
}
for (int sub = 0; sub < numLines; sub++) { for (int sub = 0; sub < numLines; sub++) {
bool isCont = (sub > 0); bool isCont = (sub > 0);
@@ -140,8 +143,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.isContinuation = isCont; lm.isContinuation = isCont;
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field; lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = absAddr;
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);
@@ -187,8 +190,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id; lm.nodeId = node.id;
lm.depth = depth; lm.depth = depth;
lm.lineKind = LineKind::Field; lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR); lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
@@ -206,8 +209,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id; lm.nodeId = node.id;
lm.depth = depth; lm.depth = depth;
lm.lineKind = LineKind::ArrayElementSeparator; lm.lineKind = LineKind::ArrayElementSeparator;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
@@ -236,8 +239,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id; lm.nodeId = node.id;
lm.depth = depth; lm.depth = depth;
lm.lineKind = LineKind::Header; lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.isRootHeader = false; lm.isRootHeader = false;
@@ -300,8 +303,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field; lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind; lm.nodeKind = node.elementKind;
lm.isArrayElement = true; lm.isArrayElement = true;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + elemAddr; lm.offsetAddr = elemAddr;
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);
@@ -353,9 +356,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.depth = childDepth; lm.depth = childDepth;
lm.lineKind = LineKind::Header; lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin( lm.offsetText = fmt::fmtOffsetMargin(
tree.baseAddress + absAddr + child.offset, false, absAddr + child.offset, false,
state.offsetHexDigits); state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + child.offset; lm.offsetAddr = absAddr + child.offset;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.nodeKind = child.kind; lm.nodeKind = child.kind;
lm.foldHead = true; lm.foldHead = true;
@@ -399,8 +402,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0; lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap); int sz = tree.structSpan(node.id, &state.childMap);
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + sz; lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
} }
@@ -445,8 +448,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id; lm.nodeId = node.id;
lm.depth = depth; lm.depth = depth;
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header; lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.foldHead = true; lm.foldHead = true;
@@ -472,26 +475,21 @@ void composeNode(ComposeState& state, const NodeTree& tree,
// Treat sentinel values as invalid pointers // Treat sentinel values as invalid pointers
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF)) if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0; ptrVal = 0;
else {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
}
} }
} }
// Determine if pointer target is actually readable // Pointer target address is used directly (absolute)
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0; uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1); bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
// For invalid/unreadable pointers: use NullProvider (shows zeros) // For invalid/unreadable pointers: use NullProvider (shows zeros)
// and reset margin offsets (unsigned wrap cancels baseAddress)
static NullProvider s_nullProv; static NullProvider s_nullProv;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv); const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
if (!ptrReadable) if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress; pBase = 0;
uint64_t savedPtrBase = state.currentPtrBase; uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = tree.baseAddress + pBase; state.currentPtrBase = pBase;
if (hasMaterialized) { if (hasMaterialized) {
// Render materialized children at the pointer target address. // Render materialized children at the pointer target address.
@@ -566,16 +564,16 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
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);
// Precompute absolute offsets // 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++)
state.absOffsets[i] = tree.computeOffset(i); state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
// Compute hex digit tier from max absolute address // Compute hex digit tier from max absolute address
{ {
uint64_t maxAddr = tree.baseAddress; uint64_t maxAddr = tree.baseAddress;
for (int i = 0; i < tree.nodes.size(); i++) { for (int i = 0; i < tree.nodes.size(); i++) {
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i]; uint64_t addr = (uint64_t)state.absOffsets[i];
if (addr > maxAddr) maxAddr = addr; if (addr > maxAddr) maxAddr = addr;
} }
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4; if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;

View File

@@ -1,6 +1,7 @@
#include "controller.h" #include "controller.h"
#include "typeselectorpopup.h" #include "typeselectorpopup.h"
#include "providerregistry.h" #include "providerregistry.h"
#include "themes/thememanager.h"
#include <Qsci/qsciscintilla.h> #include <Qsci/qsciscintilla.h>
#include <QSplitter> #include <QSplitter>
#include <QFile> #include <QFile>
@@ -222,14 +223,24 @@ void RcxController::connectEditor(RcxEditor* editor) {
TypePopupMode mode = TypePopupMode::FieldType; TypePopupMode mode = TypePopupMode::FieldType;
if (target == EditTarget::ArrayElementType) if (target == EditTarget::ArrayElementType)
mode = TypePopupMode::ArrayElement; mode = TypePopupMode::ArrayElement;
else if (target == EditTarget::PointerTarget) else if (target == EditTarget::PointerTarget) {
mode = TypePopupMode::PointerTarget; // Primitive pointers (ptrDepth>0) should open FieldType with
// the base type selected and *//** preselected — not PointerTarget.
bool isPrimPtr = false;
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
const auto& n = m_doc->tree.nodes[nodeIdx];
isPrimPtr = n.ptrDepth > 0 && n.refId == 0;
}
mode = isPrimPtr ? TypePopupMode::FieldType
: TypePopupMode::PointerTarget;
}
showTypePopup(editor, mode, nodeIdx, globalPos); showTypePopup(editor, mode, nodeIdx, globalPos);
}); });
// Inline editing signals // Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted, connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
uint64_t resolvedAddr) {
// CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1 // CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source
&& target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; } && target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; }
@@ -240,7 +251,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
// ASCII edit on Hex nodes // ASCII edit on Hex nodes
if (isHexPreview(node.kind)) { if (isHexPreview(node.kind)) {
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true); setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
} else { } else {
renameNode(nodeIdx, text); renameNode(nodeIdx, text);
} }
@@ -310,7 +321,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
break; break;
} }
case EditTarget::Value: case EditTarget::Value:
setNodeValue(nodeIdx, subLine, text); setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
break; break;
case EditTarget::BaseAddress: { case EditTarget::BaseAddress: {
QString s = text.trimmed(); QString s = text.trimmed();
@@ -367,125 +378,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
} }
break; break;
} }
case EditTarget::Source: { case EditTarget::Source:
if (text.startsWith(QStringLiteral("#saved:"))) { selectSource(text);
int idx = text.mid(7).toInt();
switchToSavedSource(idx);
} else if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) {
// Save current source's base address before switching
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
m_doc->loadData(path);
// Check if this file is already saved
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == QStringLiteral("File")
&& m_savedSources[i].filePath == path) {
existingIdx = i;
break; break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_doc->tree.baseAddress = m_savedSources[existingIdx].baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = QFileInfo(path).fileName();
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
}
}
else
{
// Look up provider in registry
const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", ""));
if (providerInfo) {
QString target;
bool selected = false;
// Execute provider's target selection
if (providerInfo->isBuiltin) {
// Built-in provider with factory function
if (providerInfo->factory) {
selected = providerInfo->factory(qobject_cast<QWidget*>(parent()), &target);
}
} else {
// Plugin-based provider
if (providerInfo->plugin) {
selected = providerInfo->plugin->selectTarget(qobject_cast<QWidget*>(parent()), &target);
}
}
if (selected && !target.isEmpty()) {
// Create provider from target
std::unique_ptr<Provider> provider;
QString errorMsg;
if (providerInfo->plugin)
{
provider = providerInfo->plugin->createProvider(target, &errorMsg);
}
// Apply provider or show error
if (provider) {
// Save current source's base address before switching
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
uint64_t newBase = provider->base();
QString displayName = provider->name();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
resetSnapshot();
emit m_doc->documentChanged();
// Save as a source for quick-switch
QString identifier = providerInfo->identifier;
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == identifier
&& m_savedSources[i].providerTarget == target) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_savedSources[existingIdx].baseAddress = m_doc->tree.baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = identifier;
entry.displayName = displayName;
entry.providerTarget = target;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
} else if (!errorMsg.isEmpty()) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
}
}
}
}
break;
}
case EditTarget::ArrayElementType: { case EditTarget::ArrayElementType: {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
@@ -605,6 +500,16 @@ void RcxController::scrollToNodeId(uint64_t nodeId) {
editor->scrollToNodeId(nodeId); editor->scrollToNodeId(nodeId);
} }
void RcxController::setTrackValues(bool on) {
m_trackValues = on;
if (!on) {
m_valueHistory.clear();
for (auto& lm : m_lastResult.meta)
lm.heatLevel = 0;
refresh();
}
}
void RcxController::refresh() { void RcxController::refresh() {
// Bracket compose with thread-local doc pointer for type name resolution // Bracket compose with thread-local doc pointer for type name resolution
s_composeDoc = m_doc; s_composeDoc = m_doc;
@@ -657,7 +562,7 @@ void RcxController::refresh() {
else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive()) else if (m_doc->provider && m_doc->provider->isValid() && m_doc->provider->isLive())
prov = m_doc->provider.get(); prov = m_doc->provider.get();
if (prov) { if (m_trackValues && prov) {
for (auto& lm : m_lastResult.meta) { for (auto& lm : m_lastResult.meta) {
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue; if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
if (isSyntheticLine(lm) || lm.isContinuation) continue; if (isSyntheticLine(lm) || lm.isContinuation) continue;
@@ -671,10 +576,7 @@ void RcxController::refresh() {
if (isFuncPtr(node.kind)) continue; if (isFuncPtr(node.kind)) continue;
// Use the absolute address from compose (correct for pointer-expanded nodes) // Use the absolute address from compose (correct for pointer-expanded nodes)
// and convert to provider-relative by subtracting the base address. uint64_t addr = lm.offsetAddr;
uint64_t addr = lm.offsetAddr >= m_doc->tree.baseAddress
? lm.offsetAddr - m_doc->tree.baseAddress
: static_cast<uint64_t>(m_doc->tree.computeOffset(lm.nodeIdx));
int sz = node.byteSize(); int sz = node.byteSize();
if (sz <= 0 || !prov->isReadable(addr, sz)) continue; if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
@@ -884,6 +786,48 @@ void RcxController::removeNode(int nodeIdx) {
cmd::Remove{nodeId, subtree, adjs})); cmd::Remove{nodeId, subtree, adjs}));
} }
void RcxController::deleteRootStruct(uint64_t structId) {
int ni = m_doc->tree.indexOfId(structId);
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
if (node.parentId != 0 || node.kind != NodeKind::Struct) return;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Delete root struct"));
// Clear all refId references pointing to this struct
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
auto& n = m_doc->tree.nodes[i];
if (n.refId == structId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{n.id, n.refId, (uint64_t)0}));
}
}
// Remove the struct + subtree (re-lookup since commands may shift indices)
ni = m_doc->tree.indexOfId(structId);
if (ni >= 0)
removeNode(ni);
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
// Switch view if we just deleted the viewed root
if (m_viewRootId == structId) {
uint64_t nextRoot = 0;
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
nextRoot = n.id;
break;
}
}
setViewRootId(nextRoot);
}
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];
@@ -1038,12 +982,6 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
clearHistoryForAdjs(c.offAdjs); clearHistoryForAdjs(c.offAdjs);
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) { } else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
tree.baseAddress = isUndo ? c.oldBase : c.newBase; tree.baseAddress = isUndo ? c.oldBase : c.newBase;
qDebug() << "[ChangeBase] tree.baseAddress =" << Qt::hex << tree.baseAddress
<< "provider =" << (m_doc->provider ? "yes" : "null");
if (m_doc->provider) {
m_doc->provider->setBase(tree.baseAddress);
qDebug() << "[ChangeBase] provider->base() now =" << Qt::hex << m_doc->provider->base();
}
resetSnapshot(); resetSnapshot();
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) { } else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes; const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
@@ -1095,14 +1033,22 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
} }
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text, void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii) { bool isAscii, uint64_t resolvedAddr) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return; if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
if (!m_doc->provider->isWritable()) return; if (!m_doc->provider->isWritable()) return;
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
// Use the compose-resolved address when available (correct for pointer children).
// Fall back to tree.baseAddress + computeOffset for callers that don't supply it.
uint64_t addr;
if (resolvedAddr != 0) {
addr = resolvedAddr;
} else {
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx); int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
if (signedAddr < 0) return; // malformed tree: negative offset if (signedAddr < 0) return; // malformed tree: negative offset
uint64_t addr = static_cast<uint64_t>(signedAddr); addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
}
// For vector components, redirect to float parsing at sub-offset // For vector components, redirect to float parsing at sub-offset
NodeKind editKind = node.kind; NodeKind editKind = node.kind;
@@ -1191,6 +1137,128 @@ void RcxController::duplicateNode(int nodeIdx) {
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs})); m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
} }
void RcxController::convertToTypedPointer(uint64_t nodeId) {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
// Determine pointer kind from current node size
NodeKind ptrKind;
if (node.byteSize() >= 8 || node.kind == NodeKind::Pointer64)
ptrKind = NodeKind::Pointer64;
else
ptrKind = NodeKind::Pointer32;
// Generate unique struct name: "NewClass", "NewClass_2", "NewClass_3", ...
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int suffix = 2;
while (true) {
bool exists = false;
for (const auto& n : m_doc->tree.nodes) {
if (n.kind == NodeKind::Struct && n.structTypeName == typeName) {
exists = true; break;
}
}
if (!exists) break;
typeName = QStringLiteral("%1_%2").arg(baseName).arg(suffix++);
}
// Create the new root struct node
Node rootStruct;
rootStruct.kind = NodeKind::Struct;
rootStruct.name = QStringLiteral("instance");
rootStruct.structTypeName = typeName;
rootStruct.classKeyword = QStringLiteral("class");
rootStruct.parentId = 0;
rootStruct.offset = 0;
rootStruct.id = m_doc->tree.reserveId();
// Create child Hex64 fields for the new struct
constexpr int kDefaultFields = 16;
QVector<Node> children;
for (int i = 0; i < kDefaultFields; i++) {
Node c;
c.kind = NodeKind::Hex64;
c.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
c.parentId = rootStruct.id;
c.offset = i * 8;
c.id = m_doc->tree.reserveId();
children.append(c);
}
uint64_t oldRefId = node.refId;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to ptr*"));
// 1. Change kind to Pointer64/32 (if not already)
if (node.kind != ptrKind)
changeNodeKind(ni, ptrKind);
// 2. Insert the new root struct
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{rootStruct, {}}));
// 3. Insert its children
for (const Node& c : children)
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{c, {}}));
// 4. Set refId to point to the new struct
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, oldRefId, rootStruct.id}));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
}
void RcxController::splitHexNode(uint64_t nodeId) {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
NodeKind halfKind;
int halfSize;
if (node.kind == NodeKind::Hex64) { halfKind = NodeKind::Hex32; halfSize = 4; }
else if (node.kind == NodeKind::Hex32) { halfKind = NodeKind::Hex16; halfSize = 2; }
else if (node.kind == NodeKind::Hex16) { halfKind = NodeKind::Hex8; halfSize = 1; }
else return;
uint64_t parentId = node.parentId;
int baseOffset = node.offset;
QString baseName = node.name;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Split Hex node"));
// Remove the original node
QVector<Node> subtree;
subtree.append(node);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{nodeId, subtree, {}}));
// Insert two half-sized nodes
Node lo;
lo.kind = halfKind;
lo.name = baseName;
lo.parentId = parentId;
lo.offset = baseOffset;
lo.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{lo, {}}));
Node hi;
hi.kind = halfKind;
hi.name = baseName + QStringLiteral("_hi");
hi.parentId = parentId;
hi.offset = baseOffset + halfSize;
hi.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{hi, {}}));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
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)); };
@@ -1288,6 +1356,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
batchChangeKind(collectIndices(), kindFromString(sel)); batchChangeKind(collectIndices(), kindFromString(sel));
}); });
menu.addSeparator();
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
}
menu.addSeparator(); menu.addSeparator();
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]() {
@@ -1378,6 +1453,32 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
}); });
addedQuickConvert = true; addedQuickConvert = true;
} }
// "Change to ptr*" — convert hex/void-ptr to typed pointer with auto-created class
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Hex32
|| ((node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32)
&& node.refId == 0)) {
menu.addAction("Change to ptr*", [this, nodeId]() {
convertToTypedPointer(nodeId);
});
addedQuickConvert = true;
}
// Split hex node into two half-sized hex nodes
if (node.kind == NodeKind::Hex64) {
menu.addAction("Change to hex32+hex32", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex32) {
menu.addAction("Change to hex16+hex16", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
} else if (node.kind == NodeKind::Hex16) {
menu.addAction("Change to hex8+hex8", [this, nodeId]() {
splitHexNode(nodeId);
});
addedQuickConvert = true;
}
if (addedQuickConvert) if (addedQuickConvert)
menu.addSeparator(); menu.addSeparator();
@@ -1398,6 +1499,15 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
editor->beginInlineEdit(EditTarget::Type, line); editor->beginInlineEdit(EditTarget::Type, line);
}); });
menu.addSeparator();
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
}
menu.addSeparator();
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8) // Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) { if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
menu.addAction("Convert to &Hex", [this, nodeId]() { menu.addAction("Convert to &Hex", [this, nodeId]() {
@@ -1507,6 +1617,13 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
refresh(); refresh();
}); });
menu.addSeparator();
{
auto* act = menu.addAction("Track Value Changes");
act->setCheckable(true);
act->setChecked(m_trackValues);
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]() {
@@ -1726,6 +1843,9 @@ void RcxController::updateCommandRow() {
TypeSelectorPopup* RcxController::ensurePopup(RcxEditor* editor) { TypeSelectorPopup* RcxController::ensurePopup(RcxEditor* editor) {
if (!m_cachedPopup) { if (!m_cachedPopup) {
m_cachedPopup = new TypeSelectorPopup(editor); m_cachedPopup = new TypeSelectorPopup(editor);
// Keep popup colors in sync when theme changes
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
m_cachedPopup, &TypeSelectorPopup::applyTheme);
// Pre-warm: force native window creation so first visible show is fast // Pre-warm: force native window creation so first visible show is fast
m_cachedPopup->warmUp(); m_cachedPopup->warmUp();
} }
@@ -1744,6 +1864,8 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
QVector<TypeEntry> entries; QVector<TypeEntry> entries;
TypeEntry currentEntry; TypeEntry currentEntry;
bool hasCurrent = false; bool hasCurrent = false;
int preModId = 0; // modifier to preselect: 0=plain, 1=*, 2=**, 3=[n]
int preArrayCount = 0; // array count when preModId==3
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) { auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
for (const auto& m : kKindMeta) { for (const auto& m : kKindMeta) {
@@ -1783,10 +1905,43 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}); });
break; break;
case TypePopupMode::FieldType: case TypePopupMode::FieldType: {
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false); addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
if (node) { bool isPtr = node
// Mark current primitive && (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
bool isTypedPtr = isPtr && node->refId != 0;
bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0;
bool isArray = node && node->kind == NodeKind::Array;
if (isPrimPtr) {
// Primitive pointer (e.g. int32* or f64**) — current = element kind, modifier = *//**
preModId = (node->ptrDepth >= 2) ? 2 : 1;
for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
currentEntry = e;
hasCurrent = true;
break;
}
}
} else if (isTypedPtr) {
// Typed pointer (e.g. Ball*) — current = composite target, modifier = *
preModId = 1;
} else if (isArray) {
// Array — modifier = [n]
preModId = 3;
preArrayCount = node->arrayLen;
if (node->elementKind != NodeKind::Struct) {
// Primitive array — mark element kind as current
for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
currentEntry = e;
hasCurrent = true;
break;
}
}
}
} else if (node) {
// Plain primitive — mark current
for (auto& e : entries) { for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) { if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
currentEntry = e; currentEntry = e;
@@ -1795,8 +1950,14 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
} }
} }
} }
addComposites([](const Node&, const TypeEntry&) { return false; }); // For isTypedPtr or struct-array: current is a Composite, set by addComposites below
addComposites([&](const Node& n, const TypeEntry& e) {
if (isTypedPtr && n.refId == e.structId) return true;
if (isArray && n.elementKind == NodeKind::Struct && n.refId == e.structId) return true;
return false;
});
break; break;
}
case TypePopupMode::ArrayElement: case TypePopupMode::ArrayElement:
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true); addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
@@ -1833,6 +1994,29 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
} }
} }
// ── Add types from other open documents (not for Root mode) ──
if (mode != TypePopupMode::Root && m_projectDocs) {
QSet<QString> localNames;
for (const auto& e : entries)
if (e.entryKind == TypeEntry::Composite)
localNames.insert(e.displayName);
for (auto* doc : *m_projectDocs) {
if (doc == m_doc) continue;
for (const auto& n : doc->tree.nodes) {
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
QString name = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
if (name.isEmpty() || localNames.contains(name)) continue;
localNames.insert(name);
TypeEntry e;
e.entryKind = TypeEntry::Composite;
e.structId = 0; // sentinel: not in local tree yet
e.displayName = name;
e.classKeyword = n.resolvedClassKeyword();
entries.append(e);
}
}
}
// ── Font with zoom ── // ── Font with zoom ──
QSettings settings("Reclass", "Reclass"); QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString(); QString fontName = settings.value("font", "JetBrains Mono").toString();
@@ -1860,6 +2044,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
popup->setFont(font); popup->setFont(font);
popup->setMode(mode); popup->setMode(mode);
// Preselect modifier button to reflect current node state (after setMode resets to plain)
if (preModId > 0)
popup->setModifier(preModId, preArrayCount);
// Pass current node size for same-size sorting // Pass current node size for same-size sorting
int nodeSize = 0; int nodeSize = 0;
if (node) { if (node) {
@@ -1885,9 +2073,22 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
m_suppressRefresh = true; m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Create new type")); m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
// Generate unique default type name
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : m_doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
Node n; Node n;
n.kind = NodeKind::Struct; n.kind = NodeKind::Struct;
n.name = QString(); n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0; n.parentId = 0;
n.offset = 0; n.offset = 0;
n.id = m_doc->tree.reserveId(); n.id = m_doc->tree.reserveId();
@@ -1913,9 +2114,16 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx, void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
const TypeEntry& entry, const QString& fullText) { const TypeEntry& entry, const QString& fullText) {
// Resolve external types: structId==0 means from another document, import first
TypeEntry resolved = entry;
if (resolved.entryKind == TypeEntry::Composite && resolved.structId == 0
&& !resolved.displayName.isEmpty()) {
resolved.structId = findOrCreateStructByName(resolved.displayName);
}
if (mode == TypePopupMode::Root) { if (mode == TypePopupMode::Root) {
if (entry.entryKind == TypeEntry::Composite) if (resolved.entryKind == TypeEntry::Composite)
setViewRootId(entry.structId); setViewRootId(resolved.structId);
return; return;
} }
@@ -1934,7 +2142,7 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
TypeSpec spec = parseTypeSpec(fullText); TypeSpec spec = parseTypeSpec(fullText);
if (mode == TypePopupMode::FieldType) { if (mode == TypePopupMode::FieldType) {
if (entry.entryKind == TypeEntry::Primitive) { if (resolved.entryKind == TypeEntry::Primitive) {
if (spec.arrayCount > 0) { if (spec.arrayCount > 0) {
// Primitive array: e.g. "int32_t[10]" // Primitive array: e.g. "int32_t[10]"
bool wasSuppressed = m_suppressRefresh; bool wasSuppressed = m_suppressRefresh;
@@ -1945,19 +2153,57 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
int idx = m_doc->tree.indexOfId(nodeId); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) { if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx]; auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != entry.primitiveKind || n.arrayLen != spec.arrayCount) if (n.elementKind != resolved.primitiveKind || n.arrayLen != spec.arrayCount)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, entry.primitiveKind, cmd::ChangeArrayMeta{nodeId, n.elementKind, resolved.primitiveKind,
n.arrayLen, spec.arrayCount})); n.arrayLen, spec.arrayCount}));
} }
m_doc->undoStack.endMacro(); m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed; m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh(); if (!m_suppressRefresh) refresh();
} else { } else if (spec.isPointer) {
if (entry.primitiveKind != nodeKind) if (!isValidPrimitivePtrTarget(resolved.primitiveKind)) {
changeNodeKind(nodeIdx, entry.primitiveKind); // Hex, pointer, fnptr types with * → plain void pointer
if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
n.ptrDepth = 0;
if (n.refId != 0)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, 0}));
} }
} else if (entry.entryKind == TypeEntry::Composite) { } else {
// Primitive pointer: e.g. "int32*" or "f64**" → Pointer64 + elementKind + ptrDepth
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != resolved.primitiveKind || n.ptrDepth != spec.ptrDepth) {
NodeKind oldEK = n.elementKind;
int oldDepth = n.ptrDepth;
n.elementKind = resolved.primitiveKind;
n.ptrDepth = spec.ptrDepth;
if (n.refId != 0)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, 0}));
Q_UNUSED(oldEK); Q_UNUSED(oldDepth);
}
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
} else {
if (resolved.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, resolved.primitiveKind);
}
} else if (resolved.entryKind == TypeEntry::Composite) {
bool wasSuppressed = m_suppressRefresh; bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true; m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to composite type")); m_doc->undoStack.beginMacro(QStringLiteral("Change to composite type"));
@@ -1967,9 +2213,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (nodeKind != NodeKind::Pointer64) if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64); changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId) if (idx >= 0 && m_doc->tree.nodes[idx].refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId})); cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
} else if (spec.arrayCount > 0) { } else if (spec.arrayCount > 0) {
// Array modifier: e.g. "Material[10]" → Array + Struct element // Array modifier: e.g. "Material[10]" → Array + Struct element
@@ -1982,9 +2228,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct, cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
n.arrayLen, spec.arrayCount})); n.arrayLen, spec.arrayCount}));
if (n.refId != entry.structId) if (n.refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, entry.structId})); cmd::ChangePointerRef{nodeId, n.refId, resolved.structId}));
} }
} else { } else {
@@ -1993,7 +2239,7 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
changeNodeKind(nodeIdx, NodeKind::Struct); changeNodeKind(nodeIdx, NodeKind::Struct);
int idx = m_doc->tree.indexOfId(nodeId); int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) { if (idx >= 0) {
int refIdx = m_doc->tree.indexOfId(entry.structId); int refIdx = m_doc->tree.indexOfId(resolved.structId);
QString targetName; QString targetName;
if (refIdx >= 0) { if (refIdx >= 0) {
const Node& ref = m_doc->tree.nodes[refIdx]; const Node& ref = m_doc->tree.nodes[refIdx];
@@ -2004,9 +2250,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName})); cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
// Set refId so compose can expand the referenced struct's children // Set refId so compose can expand the referenced struct's children
if (m_doc->tree.nodes[idx].refId != entry.structId) if (m_doc->tree.nodes[idx].refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId})); cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
// ChangePointerRef auto-sets collapsed=true when refId != 0 // ChangePointerRef auto-sets collapsed=true when refId != 0
} }
} }
@@ -2016,28 +2262,28 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (!m_suppressRefresh) refresh(); if (!m_suppressRefresh) refresh();
} }
} else if (mode == TypePopupMode::ArrayElement) { } else if (mode == TypePopupMode::ArrayElement) {
if (entry.entryKind == TypeEntry::Primitive) { if (resolved.entryKind == TypeEntry::Primitive) {
if (entry.primitiveKind != elemKind) { if (resolved.primitiveKind != elemKind) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, cmd::ChangeArrayMeta{nodeId,
elemKind, entry.primitiveKind, elemKind, resolved.primitiveKind,
arrLen, arrLen})); arrLen, arrLen}));
} }
} else if (entry.entryKind == TypeEntry::Composite) { } else if (resolved.entryKind == TypeEntry::Composite) {
if (elemKind != NodeKind::Struct || nodeRefId != entry.structId) { if (elemKind != NodeKind::Struct || nodeRefId != resolved.structId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, cmd::ChangeArrayMeta{nodeId,
elemKind, NodeKind::Struct, elemKind, NodeKind::Struct,
arrLen, arrLen})); arrLen, arrLen}));
if (nodeRefId != entry.structId) { if (nodeRefId != resolved.structId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, nodeRefId, entry.structId})); cmd::ChangePointerRef{nodeId, nodeRefId, resolved.structId}));
} }
} }
} }
} else if (mode == TypePopupMode::PointerTarget) { } else if (mode == TypePopupMode::PointerTarget) {
// "void" entry → refId 0; composite entry → real structId // "void" entry → refId 0; composite entry → real structId
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0; uint64_t realRefId = (resolved.entryKind == TypeEntry::Composite) ? resolved.structId : 0;
if (realRefId != nodeRefId) { if (realRefId != nodeRefId) {
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, nodeRefId, realRefId})); cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
@@ -2045,6 +2291,33 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
} }
} }
uint64_t RcxController::findOrCreateStructByName(const QString& typeName) {
// Check if it already exists locally
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& (n.structTypeName == typeName || (n.structTypeName.isEmpty() && n.name == typeName)))
return n.id;
}
// Import: create a new root struct with that name + default hex fields
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Import type"));
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0;
n.offset = 0;
n.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
for (int i = 0; i < 8; i++)
insertNode(n.id, i * 8, NodeKind::Hex64,
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
return n.id;
}
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) { void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
const auto* info = ProviderRegistry::instance().findProvider(providerIdentifier); const auto* info = ProviderRegistry::instance().findProvider(providerIdentifier);
if (!info || !info->plugin) { if (!info || !info->plugin) {
@@ -2068,8 +2341,6 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
m_doc->dataPath.clear(); m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0) if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase; m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
resetSnapshot(); resetSnapshot();
emit m_doc->documentChanged(); emit m_doc->documentChanged();
refresh(); refresh();
@@ -2096,6 +2367,117 @@ void RcxController::switchToSavedSource(int idx) {
} }
} }
void RcxController::selectSource(const QString& text) {
if (text == QStringLiteral("#clear")) {
clearSources();
} else if (text.startsWith(QStringLiteral("#saved:"))) {
int idx = text.mid(7).toInt();
switchToSavedSource(idx);
} else if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) {
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
m_doc->loadData(path);
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == QStringLiteral("File")
&& m_savedSources[i].filePath == path) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_doc->tree.baseAddress = m_savedSources[existingIdx].baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = QFileInfo(path).fileName();
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
}
} else {
const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", ""));
if (providerInfo) {
QString target;
bool selected = false;
if (providerInfo->isBuiltin) {
if (providerInfo->factory)
selected = providerInfo->factory(qobject_cast<QWidget*>(parent()), &target);
} else {
if (providerInfo->plugin)
selected = providerInfo->plugin->selectTarget(qobject_cast<QWidget*>(parent()), &target);
}
if (selected && !target.isEmpty()) {
std::unique_ptr<Provider> provider;
QString errorMsg;
if (providerInfo->plugin)
provider = providerInfo->plugin->createProvider(target, &errorMsg);
if (provider) {
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
uint64_t newBase = provider->base();
QString displayName = provider->name();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
resetSnapshot();
emit m_doc->documentChanged();
QString identifier = providerInfo->identifier;
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == identifier
&& m_savedSources[i].providerTarget == target) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_savedSources[existingIdx].baseAddress = m_doc->tree.baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = identifier;
entry.displayName = displayName;
entry.providerTarget = target;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
} else if (!errorMsg.isEmpty()) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
}
}
}
}
}
void RcxController::clearSources() {
m_savedSources.clear();
m_activeSourceIdx = -1;
m_doc->provider = std::make_shared<NullProvider>();
m_doc->dataPath.clear();
resetSnapshot();
pushSavedSourcesToEditors();
refresh();
}
void RcxController::pushSavedSourcesToEditors() { void RcxController::pushSavedSourcesToEditors() {
QVector<SavedSourceDisplay> display; QVector<SavedSourceDisplay> display;
display.reserve(m_savedSources.size()); display.reserve(m_savedSources.size());
@@ -2130,7 +2512,7 @@ void RcxController::setupAutoRefresh() {
} }
// Recursively collect memory ranges for a struct and its pointer targets. // Recursively collect memory ranges for a struct and its pointer targets.
// memBase is the provider-relative address where this struct's data lives. // memBase is the absolute address where this struct's data lives.
void RcxController::collectPointerRanges( void RcxController::collectPointerRanges(
uint64_t structId, uint64_t memBase, uint64_t structId, uint64_t memBase,
int depth, int maxDepth, int depth, int maxDepth,
@@ -2163,9 +2545,9 @@ void RcxController::collectPointerRanges(
uint64_t ptrVal = (child.kind == NodeKind::Pointer32) uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
? (uint64_t)m_snapshotProv->readU32(ptrAddr) ? (uint64_t)m_snapshotProv->readU32(ptrAddr)
: m_snapshotProv->readU64(ptrAddr); : m_snapshotProv->readU64(ptrAddr);
if (ptrVal == 0 || ptrVal == UINT64_MAX || ptrVal < m_doc->tree.baseAddress) continue; if (ptrVal == 0 || ptrVal == UINT64_MAX) continue;
uint64_t pBase = ptrVal - m_doc->tree.baseAddress; uint64_t pBase = ptrVal;
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth, collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
visited, ranges); visited, ranges);
} }
@@ -2190,16 +2572,16 @@ void RcxController::onRefreshTick() {
int extent = computeDataExtent(); int extent = computeDataExtent();
if (extent <= 0) return; if (extent <= 0) return;
// Collect all needed ranges: main struct + pointer targets // Collect all needed ranges: main struct + pointer targets (absolute addresses)
QVector<QPair<uint64_t,int>> ranges; QVector<QPair<uint64_t,int>> ranges;
ranges.append({0, extent}); ranges.append({m_doc->tree.baseAddress, extent});
if (m_snapshotProv) { if (m_snapshotProv) {
QSet<QPair<uint64_t,uint64_t>> visited; QSet<QPair<uint64_t,uint64_t>> visited;
uint64_t rootId = m_viewRootId; uint64_t rootId = m_viewRootId;
if (rootId == 0 && !m_doc->tree.nodes.isEmpty()) if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
rootId = m_doc->tree.nodes[0].id; rootId = m_doc->tree.nodes[0].id;
collectPointerRanges(rootId, 0, 0, 99, visited, ranges); collectPointerRanges(rootId, m_doc->tree.baseAddress, 0, 99, visited, ranges);
} }
m_readInFlight = true; m_readInFlight = true;

View File

@@ -92,14 +92,20 @@ public:
void removeNode(int nodeIdx); void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx); void toggleCollapse(int nodeIdx);
void materializeRefChildren(int nodeIdx); void materializeRefChildren(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false); void setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii = false, uint64_t resolvedAddr = 0);
void duplicateNode(int nodeIdx); void duplicateNode(int nodeIdx);
void convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId);
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 applyCommand(const Command& cmd, bool isUndo); void applyCommand(const Command& cmd, bool isUndo);
void refresh(); void refresh();
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
uint64_t findOrCreateStructByName(const QString& typeName);
// Selection // Selection
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId, void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
@@ -122,6 +128,15 @@ public:
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; } const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
int activeSourceIndex() const { return m_activeSourceIdx; } int activeSourceIndex() const { return m_activeSourceIdx; }
void switchSource(int idx) { switchToSavedSource(idx); } void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }
void setTrackValues(bool on);
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
// Test accessor // Test accessor
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; } const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
@@ -154,17 +169,19 @@ private:
PageMap m_prevPages; PageMap m_prevPages;
QSet<int64_t> m_changedOffsets; QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory; QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = false;
uint64_t m_refreshGen = 0; uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0; uint64_t m_readGen = 0;
bool m_readInFlight = false; bool m_readInFlight = false;
QVector<RcxDocument*>* m_projectDocs = nullptr;
void connectEditor(RcxEditor* editor); void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods); void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow(); void updateCommandRow();
void switchToSavedSource(int idx); void switchToSavedSource(int idx);
void pushSavedSourcesToEditors(); void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos); void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
TypeSelectorPopup* ensurePopup(RcxEditor* editor); TypeSelectorPopup* ensurePopup(RcxEditor* editor);
// ── Auto-refresh methods ── // ── Auto-refresh methods ──

View File

@@ -142,6 +142,15 @@ inline constexpr bool isMatrixKind(NodeKind k) {
inline constexpr bool isFuncPtr(NodeKind k) { inline constexpr bool isFuncPtr(NodeKind k) {
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64; return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
} }
// Hex types, pointer types, function pointers, and containers are not meaningful
// primitive-pointer targets — dereferencing them produces the same output as void*.
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
if (isHexNode(k)) return false;
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
if (isFuncPtr(k)) return false;
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
return true;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) { inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out; QStringList out;
@@ -184,7 +193,8 @@ struct Node {
int strLen = 64; int strLen = 64;
bool collapsed = false; bool collapsed = false;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
NodeKind elementKind = NodeKind::UInt8; // Array: element 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 viewIndex = 0; // Array: current view offset (transient) int viewIndex = 0; // Array: current view offset (transient)
// 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.
@@ -217,6 +227,8 @@ struct Node {
o["collapsed"] = collapsed; o["collapsed"] = collapsed;
o["refId"] = QString::number(refId); o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind); o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
return o; return o;
} }
static Node fromJson(const QJsonObject& o) { static Node fromJson(const QJsonObject& o) {
@@ -233,6 +245,7 @@ struct Node {
n.collapsed = o["collapsed"].toBool(false); n.collapsed = o["collapsed"].toBool(false);
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);
return n; return n;
} }

View File

@@ -20,6 +20,7 @@
#include <QLabel> #include <QLabel>
#include <QToolButton> #include <QToolButton>
#include <QScreen> #include <QScreen>
#include <QScrollBar>
#include <functional> #include <functional>
#include "themes/thememanager.h" #include "themes/thememanager.h"
@@ -255,6 +256,103 @@ public:
} }
}; };
class StructPreviewPopup : public QFrame {
uint64_t m_nodeId = 0;
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
public:
explicit StructPreviewPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
m_titleLabel = new QLabel;
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
vbox->addWidget(m_titleLabel);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
vbox->addWidget(sep);
m_bodyLabel = new QLabel;
m_bodyLabel->setTextFormat(Qt::PlainText);
m_bodyLabel->setWordWrap(false);
vbox->addWidget(m_bodyLabel);
}
uint64_t nodeId() const { return m_nodeId; }
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
return;
m_nodeId = nodeId;
m_body = body;
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
QFont bold = font;
bold.setBold(true);
m_titleLabel->setFont(bold);
m_titleLabel->setText(title);
m_titleLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
for (auto* child : findChildren<QFrame*>()) {
if (child->frameShape() == QFrame::HLine) {
QPalette sp;
sp.setColor(QPalette::WindowText, theme.border);
child->setPalette(sp);
break;
}
}
m_bodyLabel->setFont(font);
m_bodyLabel->setText(body);
m_bodyLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
setMaximumWidth(600);
adjustSize();
}
void showAt(const QPoint& globalPos) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - 4;
move(x, y);
if (!isVisible()) show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_body.clear();
}
};
static constexpr int IND_EDITABLE = 8; static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9; static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
@@ -297,6 +395,24 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
m_sci->viewport()->installEventFilter(this); m_sci->viewport()->installEventFilter(this);
m_sci->viewport()->setMouseTracking(true); m_sci->viewport()->setMouseTracking(true);
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this]() {
if (m_editState.active || !m_hoverInside) return;
m_lastHoverPos = m_sci->viewport()->mapFromGlobal(QCursor::pos());
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
auto h = hitTest(m_lastHoverPos);
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
int newHoverLine = (m_hoverInside && h.line >= 0) ? h.line : -1;
if (newHoverId != m_hoveredNodeId || newHoverLine != m_hoveredLine) {
m_hoveredNodeId = newHoverId;
m_hoveredLine = newHoverLine;
applyHoverHighlight();
}
applyHoverCursor();
});
// Hover cursor is applied synchronously in eventFilter (no timer). // Hover cursor is applied synchronously in eventFilter (no timer).
connect(m_sci, &QsciScintilla::marginClicked, connect(m_sci, &QsciScintilla::marginClicked,
@@ -372,8 +488,10 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (id == 1 && (m_editState.target == EditTarget::Type if (id == 1 && (m_editState.target == EditTarget::Type
|| m_editState.target == EditTarget::ArrayElementType || m_editState.target == EditTarget::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget)) { || m_editState.target == EditTarget::PointerTarget)) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} }
}); });
@@ -690,6 +808,10 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0'))); int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
(unsigned long)qMax(1, pixelWidth)); (unsigned long)qMax(1, pixelWidth));
// Reset horizontal scroll to 0. The controller's restoreViewState()
// will set it back to the (clamped) saved position afterward.
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)0);
} }
// Force full re-lex to fix stale syntax coloring after edits // Force full re-lex to fix stale syntax coloring after edits
@@ -1012,8 +1134,13 @@ void RcxEditor::restoreViewState(const ViewState& vs) {
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos); m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE, m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
(unsigned long)vs.scrollLine); (unsigned long)vs.scrollLine);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, // Clamp xOffset so it doesn't exceed the current content width.
(unsigned long)vs.xOffset); // After a rename that shrinks content, the saved offset may be stale.
int scrollW = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
int vpW = m_sci->viewport() ? m_sci->viewport()->width() : 0;
int maxXOff = qMax(0, scrollW - vpW);
int xOff = qBound(0, vs.xOffset, maxXOff);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)xOff);
} }
const LineMeta* RcxEditor::metaForLine(int line) const { const LineMeta* RcxEditor::metaForLine(int line) const {
@@ -1399,7 +1526,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
switch (t) { switch (t) {
case EditTarget::Type: s = typeSpan(*lm, typeW); break; case EditTarget::Type: s = typeSpan(*lm, typeW); break;
case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break; case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break;
case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break; case EditTarget::Value: s = narrowPtrValueSpan(*lm,
valueSpan(*lm, textLen, typeW, nameW), lineText); break;
case EditTarget::BaseAddress: break; // No longer on header lines case EditTarget::BaseAddress: break; // No longer on header lines
case EditTarget::ArrayIndex: case EditTarget::ArrayIndex:
case EditTarget::ArrayCount: case EditTarget::ArrayCount:
@@ -1670,15 +1798,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Single-click on editable token of already-selected node → edit // Single-click on editable token of already-selected node → edit
int tLine, tCol; EditTarget t; int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) { if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
// Type/ArrayElementType/PointerTarget open a dismissible popup if (alreadySelected && plain) {
// (not inline text edit), so allow on first click without
// requiring the node to be pre-selected.
bool isPopupTarget = (t == EditTarget::Type
|| t == EditTarget::ArrayElementType
|| t == EditTarget::PointerTarget);
if ((alreadySelected || isPopupTarget) && plain) {
if (!alreadySelected)
emit nodeClicked(h.line, h.nodeId, me->modifiers());
m_pendingClickNodeId = 0; m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine, tCol); return beginInlineEdit(t, tLine, tCol);
} }
@@ -2012,9 +2132,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
m_hoveredLine = -1; m_hoveredLine = -1;
applyHoverHighlight(); applyHoverHighlight();
// Dismiss hover popup so it gets recreated with Set buttons once edit starts // Dismiss hover popups so they get recreated with Set buttons once edit starts
if (m_historyPopup) if (m_historyPopup)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
if (m_structPreviewPopup)
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
// Clear editable-token color hints (de-emphasize non-active tokens) // Clear editable-token color hints (de-emphasize non-active tokens)
clearIndicatorLine(IND_EDITABLE, m_hintLine); clearIndicatorLine(IND_EDITABLE, m_hintLine);
m_hintLine = -1; m_hintLine = -1;
@@ -2250,8 +2372,12 @@ void RcxEditor::commitInlineEdit() {
if (m_editState.target == EditTarget::Type && editedText.isEmpty()) if (m_editState.target == EditTarget::Type && editedText.isEmpty())
editedText = m_editState.original; editedText = m_editState.original;
// Grab resolved address from LineMeta before endInlineEdit clears state
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText, addr);
} }
// ── Cancel inline edit ── // ── Cancel inline edit ──
@@ -2337,6 +2463,9 @@ void RcxEditor::showSourcePicker() {
act->setChecked(m_savedSourceDisplay[i].active); act->setChecked(m_savedSourceDisplay[i].active);
act->setData(i); act->setData(i);
} }
menu.addSeparator();
auto* clearAct = menu.addAction("Clear All");
clearAct->setData(QStringLiteral("#clear"));
} }
int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0); int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0);
@@ -2348,11 +2477,15 @@ void RcxEditor::showSourcePicker() {
QAction* sel = menu.exec(pos); QAction* sel = menu.exec(pos);
if (sel) { if (sel) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit(); auto info = endInlineEdit();
QString text = sel->text(); QString text = sel->text();
if (sel->data().isValid()) if (sel->data().toString() == QStringLiteral("#clear"))
text = QStringLiteral("#clear");
else if (sel->data().isValid())
text = QStringLiteral("#saved:") + QString::number(sel->data().toInt()); text = QStringLiteral("#saved:") + QString::number(sel->data().toInt());
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} else { } else {
cancelInlineEdit(); cancelInlineEdit();
} }
@@ -2580,9 +2713,11 @@ void RcxEditor::applyHoverCursor() {
if (!showPopup && m_historyPopup && m_historyPopup->isVisible()) if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
} }
// Always dismiss disasm popup during inline editing // Always dismiss disasm/preview popups during inline editing
if (m_disasmPopup && m_disasmPopup->isVisible()) if (m_disasmPopup && m_disasmPopup->isVisible())
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss(); static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
return; return;
} }
@@ -2593,6 +2728,8 @@ void RcxEditor::applyHoverCursor() {
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
if (m_disasmPopup && !m_applyingDocument) if (m_disasmPopup && !m_applyingDocument)
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss(); static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup && !m_applyingDocument)
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
m_sci->viewport()->setCursor(Qt::ArrowCursor); m_sci->viewport()->setCursor(Qt::ArrowCursor);
return; return;
} }
@@ -2755,11 +2892,8 @@ void RcxEditor::applyHoverCursor() {
if (!isVoidPtr || node.refId == 0) { if (!isVoidPtr || node.refId == 0) {
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64 bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|| lm.nodeKind == NodeKind::Pointer64); || lm.nodeKind == NodeKind::Pointer64);
// Use composed address (correct for pointer-expanded nodes) // Use composed address (absolute, correct for pointer-expanded nodes)
// not node.offset (which is just offset within struct definition). uint64_t provAddr = lm.offsetAddr;
uint64_t provAddr = lm.offsetAddr >= m_disasmTree->baseAddress
? lm.offsetAddr - m_disasmTree->baseAddress
: static_cast<uint64_t>(node.offset);
uint64_t ptrVal = is64 uint64_t ptrVal = is64
? m_disasmProvider->readU64(provAddr) ? m_disasmProvider->readU64(provAddr)
: (uint64_t)m_disasmProvider->readU32(provAddr); : (uint64_t)m_disasmProvider->readU32(provAddr);
@@ -2768,13 +2902,11 @@ void RcxEditor::applyHoverCursor() {
// Read code bytes from the function target address. // Read code bytes from the function target address.
// Use the real provider (not snapshot) because function // Use the real provider (not snapshot) because function
// code lives at arbitrary process addresses that aren't // code lives at arbitrary process addresses that aren't
// in the snapshot page table. The provider reads from // in the snapshot page table.
// m_base + addr via ReadProcessMemory, so we convert
// the absolute ptrVal to provider-relative.
const Provider* codeProv = m_disasmRealProv const Provider* codeProv = m_disasmRealProv
? m_disasmRealProv : m_disasmProvider; ? m_disasmRealProv : m_disasmProvider;
constexpr int kMaxRead = 128; constexpr int kMaxRead = 128;
uint64_t codeAddr = ptrVal - m_disasmTree->baseAddress; uint64_t codeAddr = ptrVal;
QByteArray bytes(kMaxRead, Qt::Uninitialized); QByteArray bytes(kMaxRead, Qt::Uninitialized);
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead); bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
if (readOk) { if (readOk) {
@@ -2837,6 +2969,70 @@ void RcxEditor::applyHoverCursor() {
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss(); static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
} }
// Struct preview popup for collapsed typed pointers
{
bool showPreview = false;
if (m_disasmTree && m_disasmProvider && h.line >= 0 && h.line < m_meta.size()) {
const LineMeta& lm = m_meta[h.line];
bool isTypedPtr = (lm.nodeKind == NodeKind::Pointer32
|| lm.nodeKind == NodeKind::Pointer64)
&& !lm.pointerTargetName.isEmpty();
if (isTypedPtr && lm.foldCollapsed
&& lm.nodeIdx >= 0 && lm.nodeIdx < m_disasmTree->nodes.size()) {
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
if (node.refId != 0) {
QString lineText = getLineText(m_sci, h.line);
ColumnSpan vs = narrowPtrValueSpan(lm,
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
lineText);
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
ComposeResult cr = rcx::compose(*m_disasmTree, *m_disasmProvider, node.refId);
// Skip command row (line 0), take first 5 data lines
QStringList lines = cr.text.split('\n');
constexpr int kMaxLines = 5;
QString body;
int count = 0;
for (int i = 1; i < lines.size() && count < kMaxLines; ++i) {
if (!lines[i].isEmpty()) {
if (count > 0) body += '\n';
body += lines[i];
++count;
}
}
if (!body.isEmpty()) {
if (!m_structPreviewPopup)
m_structPreviewPopup = new StructPreviewPopup(this);
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
popup->populate(lm.nodeId,
lm.pointerTargetName, body, editorFont());
long linePos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)h.line);
long byteOff = lineText.left(vs.start).toUtf8().size();
int px = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTXFROMPOSITION,
(unsigned long)0, linePos + byteOff);
int py = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTYFROMPOSITION,
(unsigned long)0, linePos);
int lh = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_TEXTHEIGHT,
(unsigned long)h.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(
QPoint(px, py + lh));
popup->showAt(anchor);
showPreview = true;
if (m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
}
}
}
}
if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible())
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
}
// Determine cursor shape based on interaction type // Determine cursor shape based on interaction type
Qt::CursorShape desired = Qt::ArrowCursor; Qt::CursorShape desired = Qt::ArrowCursor;

View File

@@ -27,6 +27,7 @@ public:
void restoreViewState(const ViewState& vs); void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; } QsciScintilla* scintilla() const { return m_sci; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const; const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const; int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId); void scrollToNodeId(uint64_t nodeId);
@@ -68,7 +69,8 @@ signals:
void keywordConvertRequested(const QString& newKeyword); void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods); void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine, void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text); EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled(); void inlineEditCancelled();
void typeSelectorRequested(); void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
@@ -138,6 +140,7 @@ private:
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr; const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp) QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp) QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr; const NodeTree* m_disasmTree = nullptr;

View File

@@ -267,6 +267,30 @@ static QString readValueImpl(const Node& node, const Provider& prov,
} }
case NodeKind::Pointer64: { case NodeKind::Pointer64: {
uint64_t val = prov.readU64(addr); uint64_t val = prov.readU64(addr);
// Primitive pointer: dereference and show target value
// (hex/ptr/fnptr targets fall through to plain void* display)
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
uint64_t target = val;
for (int d = 1; d < node.ptrDepth && target != 0; d++)
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
// Create a temporary node of the target kind to format the value
Node tmp;
tmp.kind = node.elementKind;
tmp.strLen = node.strLen;
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
if (display) {
QString arrow = QStringLiteral("-> ");
QString sym = prov.getSymbol(val);
if (!sym.isEmpty())
return arrow + derefVal + QStringLiteral(" // ") + sym;
return arrow + derefVal;
}
return derefVal;
}
if (!display) return rawHex(val, 16);
return fmtPointer64(val);
}
if (!display) return rawHex(val, 16); if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val); QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val); QString sym = prov.getSymbol(val);

View File

@@ -1,4 +1,5 @@
#include "mainwindow.h" #include "mainwindow.h"
#include "providerregistry.h"
#include "generator.h" #include "generator.h"
#include "import_reclass_xml.h" #include "import_reclass_xml.h"
#include "import_source.h" #include "import_source.h"
@@ -44,6 +45,8 @@
#include <Qsci/qscilexercpp.h> #include <Qsci/qscilexercpp.h>
#include <QProxyStyle> #include <QProxyStyle>
#include <QDesktopServices> #include <QDesktopServices>
#include <QWindow>
#include <QMouseEvent>
#include "themes/thememanager.h" #include "themes/thememanager.h"
#include "themes/themeeditor.h" #include "themes/themeeditor.h"
#include "optionsdialog.h" #include "optionsdialog.h"
@@ -205,6 +208,9 @@ public:
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough // Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
if (elem == PE_FrameMenu) if (elem == PE_FrameMenu)
return; return;
// Kill the status bar item frame and panel border
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
return;
QProxyStyle::drawPrimitive(elem, opt, p, w); QProxyStyle::drawPrimitive(elem, opt, p, w);
} }
void drawControl(ControlElement element, const QStyleOption* opt, void drawControl(ControlElement element, const QStyleOption* opt,
@@ -321,6 +327,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
overlay->show(); overlay->show();
m_mdiArea = new QMdiArea(this); m_mdiArea = new QMdiArea(this);
m_mdiArea->setFrameShape(QFrame::NoFrame);
m_mdiArea->setViewMode(QMdiArea::TabbedView); m_mdiArea->setViewMode(QMdiArea::TabbedView);
m_mdiArea->setTabsClosable(true); m_mdiArea->setTabsClosable(true);
m_mdiArea->setTabsMovable(true); m_mdiArea->setTabsMovable(true);
@@ -341,6 +348,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createMenus(); createMenus();
createStatusBar(); createStatusBar();
// Eliminate gap between central widget and status bar
if (auto* ml = layout()) {
ml->setSpacing(0);
ml->setContentsMargins(0, 0, 0, 0);
}
// Separator line between central widget and status bar is killed in MenuBarStyle::drawControl
// 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");
@@ -376,6 +390,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
for (int i = 0; i < tab->panes.size(); ++i) { for (int i = 0; i < tab->panes.size(); ++i) {
if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) { if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) {
tab->activePaneIdx = i; tab->activePaneIdx = i;
syncViewButtons(tab->panes[i].viewMode);
return; return;
} }
} }
@@ -407,6 +422,9 @@ 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");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
file->addSeparator();
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
file->addSeparator(); file->addSeparator();
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp); Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
@@ -492,11 +510,188 @@ void MainWindow::createMenus() {
Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about); Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about);
} }
// ── Themed resize grip (replaces ugly default QSizeGrip) ──
// Positioned as a direct child of MainWindow at the bottom-right corner,
// NOT inside the status bar layout (which is font-height dependent).
class ResizeGrip : public QWidget {
public:
static constexpr int kSize = 16; // widget size
static constexpr int kPad = 4; // padding from window corner (identical right & bottom)
explicit ResizeGrip(QWidget* parent) : QWidget(parent) {
setFixedSize(kSize, kSize);
setCursor(Qt::SizeFDiagCursor);
m_color = rcx::ThemeManager::instance().current().textFaint;
}
void setGripColor(const QColor& c) { m_color = c; update(); }
// Call from parent's resizeEvent to pin to bottom-right corner
void reposition() {
QWidget* w = parentWidget();
if (w) move(w->width() - kSize - kPad, w->height() - kSize - kPad);
}
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setPen(Qt::NoPen);
p.setBrush(m_color);
// 6 dots in a triangle pointing bottom-right (VS2022 style)
// Dot grid is centered within the widget: same inset from right and bottom
const double r = 1.0, s = 4.0;
const double inset = 4.0;
double bx = width() - inset;
double by = height() - inset;
// bottom row: 3 dots
p.drawEllipse(QPointF(bx, by), r, r);
p.drawEllipse(QPointF(bx - s, by), r, r);
p.drawEllipse(QPointF(bx - 2 * s, by), r, r);
// middle row: 2 dots
p.drawEllipse(QPointF(bx, by - s), r, r);
p.drawEllipse(QPointF(bx - s, by - s), r, r);
// top row: 1 dot
p.drawEllipse(QPointF(bx, by - 2 * s), r, r);
}
void mousePressEvent(QMouseEvent* e) override {
if (e->button() == Qt::LeftButton) {
window()->windowHandle()->startSystemResize(Qt::BottomEdge | Qt::RightEdge);
e->accept();
}
}
private:
QColor m_color;
};
// ── Custom-painted view tab button (no CSS) ──
class ViewTabButton : public QPushButton {
public:
static constexpr int kAccentH = 2; // accent line height in pixels
static constexpr int kPadLR = 12; // horizontal padding
static constexpr int kPadBot = 4; // extra bottom padding
QColor colBg, colBgChecked, colBgHover, colBgPressed;
QColor colText, colTextMuted, colAccent;
explicit ViewTabButton(const QString& text, QWidget* parent = nullptr)
: QPushButton(text, parent) {
setCheckable(true);
setFlat(true);
setCursor(Qt::PointingHandCursor);
setContentsMargins(0, 0, 0, 0);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
}
QSize sizeHint() const override {
QFontMetrics fm(font());
int w = fm.horizontalAdvance(text()) + 2 * kPadLR;
int h = qRound((fm.height() + kAccentH + kPadBot) * 1.33);
return QSize(w, h);
}
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
// Background
QColor bg = colBg;
if (isDown()) bg = colBgPressed;
else if (underMouse()) bg = colBgHover;
else if (isChecked()) bg = colBgChecked;
p.fillRect(rect(), bg);
// Accent line at y=0 when checked
if (isChecked())
p.fillRect(0, 0, width(), kAccentH, colAccent);
// Text
p.setPen(isChecked() || underMouse() || isDown() ? colText : colTextMuted);
p.setFont(font());
QRect textRect(kPadLR, kAccentH, width() - 2 * kPadLR, height() - kAccentH);
p.drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, text());
}
void enterEvent(QEnterEvent*) override { update(); }
void leaveEvent(QEvent*) override { update(); }
};
// ── Borderless status bar with manual child layout ──
// QStatusBarLayout hardcodes 2px margins that can't be overridden.
// We bypass it entirely: children are placed manually in resizeEvent,
// and addWidget() is NOT used. Instead, create children as direct
// children and call manualLayout() to position them.
class FlatStatusBar : public QStatusBar {
public:
QWidget* tabRow = nullptr; // set by createStatusBar
QLabel* label = nullptr; // set by createStatusBar
explicit FlatStatusBar(QWidget* parent = nullptr) : QStatusBar(parent) {
setSizeGripEnabled(false);
}
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
p.fillRect(rect(), palette().window());
}
void resizeEvent(QResizeEvent* e) override {
QStatusBar::resizeEvent(e);
manualLayout();
}
void showEvent(QShowEvent* e) override {
QStatusBar::showEvent(e);
manualLayout();
}
private:
void manualLayout() {
if (!tabRow || !label) return;
int h = height();
int tw = tabRow->sizeHint().width();
tabRow->setGeometry(0, 0, tw, h);
label->setGeometry(tw, 0, width() - tw, h);
}
};
void MainWindow::createStatusBar() { void MainWindow::createStatusBar() {
m_statusLabel = new QLabel("Ready"); // Replace the default QStatusBar with our borderless, manually-laid-out one.
// QStatusBarLayout hardcodes 2px margins; we bypass addWidget entirely.
auto* sb = new FlatStatusBar;
setStatusBar(sb);
m_statusLabel = new QLabel("Ready", sb);
m_statusLabel->setContentsMargins(10, 0, 0, 0); m_statusLabel->setContentsMargins(10, 0, 0, 0);
statusBar()->setContentsMargins(0, 4, 0, 4);
statusBar()->addWidget(m_statusLabel, 1); // View toggle buttons (Reclass / C/C++) — custom painted, no CSS
m_viewBtnGroup = new QButtonGroup(this);
m_viewBtnGroup->setExclusive(true);
m_btnReclass = new ViewTabButton("Reclass");
m_btnReclass->setChecked(true);
m_btnRendered = new ViewTabButton("C/C++");
m_viewBtnGroup->addButton(m_btnReclass, 0);
m_viewBtnGroup->addButton(m_btnRendered, 1);
// Wrap buttons in a zero-margin container — direct child of status bar
auto* tabRow = new QWidget(sb);
auto* tabLay = new QHBoxLayout(tabRow);
tabLay->setContentsMargins(0, 0, 0, 0);
tabLay->setSpacing(0);
tabLay->addWidget(m_btnReclass);
tabLay->addWidget(m_btnRendered);
sb->tabRow = tabRow;
sb->label = m_statusLabel;
connect(m_viewBtnGroup, &QButtonGroup::idClicked, this, [this](int id) {
setViewMode(id == 1 ? VM_Rendered : VM_Reclass);
});
// Grip is a direct child of the main window, NOT in the status bar layout.
// Positioned via reposition() in resizeEvent — immune to font/margin changes.
auto* grip = new ResizeGrip(this);
grip->setObjectName("resizeGrip");
grip->raise();
{ {
const auto& t = ThemeManager::instance().current(); const auto& t = ThemeManager::instance().current();
QPalette sbPal = statusBar()->palette(); QPalette sbPal = statusBar()->palette();
@@ -504,22 +699,31 @@ void MainWindow::createStatusBar() {
sbPal.setColor(QPalette::WindowText, t.textDim); sbPal.setColor(QPalette::WindowText, t.textDim);
statusBar()->setPalette(sbPal); statusBar()->setPalette(sbPal);
statusBar()->setAutoFillBackground(true); statusBar()->setAutoFillBackground(true);
auto applyViewTabColors = [&](ViewTabButton* btn) {
btn->colBg = t.background;
btn->colBgChecked = t.backgroundAlt;
btn->colBgHover = t.hover;
btn->colBgPressed = t.hover.darker(130);
btn->colText = t.text;
btn->colTextMuted = t.textMuted;
btn->colAccent = t.indHoverSpan;
};
applyViewTabColors(static_cast<ViewTabButton*>(m_btnReclass));
applyViewTabColors(static_cast<ViewTabButton*>(m_btnRendered));
}
// Sync status bar font with editor font at startup
{
QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
statusBar()->setFont(f);
m_btnReclass->setFont(f);
m_btnRendered->setFont(f);
} }
} }
void MainWindow::applyTabWidgetStyle(QTabWidget* tw) {
const auto& t = ThemeManager::instance().current();
tw->setStyleSheet(QStringLiteral(
"QTabWidget::pane { border: none; }"
"QTabBar::tab {"
" background: %1; color: %2; padding: 4px 12px; border: none; min-width: 60px;"
"}"
"QTabBar::tab:selected { color: %3; }"
"QTabBar::tab:hover { color: %3; background: %4; }")
.arg(t.background.name(), t.textMuted.name(),
t.text.name(), t.hover.name()));
tw->tabBar()->setExpanding(false);
}
void MainWindow::styleTabCloseButtons() { void MainWindow::styleTabCloseButtons() {
auto* tabBar = m_mdiArea->findChild<QTabBar*>(); auto* tabBar = m_mdiArea->findChild<QTabBar*>();
@@ -557,7 +761,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
pane.tabWidget = new QTabWidget; pane.tabWidget = new QTabWidget;
pane.tabWidget->setTabPosition(QTabWidget::South); pane.tabWidget->setTabPosition(QTabWidget::South);
applyTabWidgetStyle(pane.tabWidget); pane.tabWidget->tabBar()->setVisible(false);
pane.tabWidget->setDocumentMode(true); // kill QTabWidget frame border
// 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);
@@ -574,18 +779,20 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
// Add to splitter // Add to splitter
tab.splitter->addWidget(pane.tabWidget); tab.splitter->addWidget(pane.tabWidget);
// Connect per-pane tab bar switching // Connect per-pane page switching (driven by status bar buttons via setViewMode)
QTabWidget* tw = pane.tabWidget; QTabWidget* tw = pane.tabWidget;
connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) { connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) {
// Find which pane this QTabWidget belongs to
SplitPane* p = findPaneByTabWidget(tw); SplitPane* p = findPaneByTabWidget(tw);
if (!p) return; if (!p) return;
if (index == 1) p->viewMode = VM_Rendered; p->viewMode = (index == 1) ? VM_Rendered : VM_Reclass;
else p->viewMode = VM_Reclass;
// Sync status bar buttons if this is the active pane
auto* tab = activeTab();
if (tab && &tab->panes[tab->activePaneIdx] == p)
syncViewButtons(p->viewMode);
if (index == 1) { if (index == 1) {
// Find the TabState that owns this pane and update rendered view
for (auto& tab : m_tabs) { for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) { for (auto& pane : tab.panes) {
if (&pane == p) { if (&pane == p) {
@@ -642,6 +849,7 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) { QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal); auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1);
auto* ctrl = new RcxController(doc, splitter); auto* ctrl = new RcxController(doc, splitter);
auto* sub = m_mdiArea->addSubWindow(splitter); auto* sub = m_mdiArea->addSubWindow(splitter);
@@ -657,12 +865,17 @@ 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));
// Give every controller the shared document list for cross-tab type visibility
ctrl->setProjectDocuments(&m_allDocs);
rebuildAllDocs();
connect(sub, &QObject::destroyed, this, [this, sub]() { connect(sub, &QObject::destroyed, this, [this, sub]() {
auto it = m_tabs.find(sub); auto it = m_tabs.find(sub);
if (it != m_tabs.end()) { if (it != m_tabs.end()) {
it->doc->deleteLater(); it->doc->deleteLater();
m_tabs.erase(it); m_tabs.erase(it);
} }
rebuildAllDocs();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
}); });
@@ -1034,6 +1247,9 @@ void MainWindow::toggleMcp() {
void MainWindow::applyTheme(const Theme& theme) { void MainWindow::applyTheme(const Theme& theme) {
applyGlobalTheme(theme); applyGlobalTheme(theme);
// Kill the 1px separator line between central widget and status bar
setStyleSheet("QMainWindow::separator { height: 0px; width: 0px; }");
// Custom title bar // Custom title bar
m_titleBar->applyTheme(theme); m_titleBar->applyTheme(theme);
@@ -1060,6 +1276,24 @@ void MainWindow::applyTheme(const Theme& theme) {
sbPal.setColor(QPalette::WindowText, theme.textDim); sbPal.setColor(QPalette::WindowText, theme.textDim);
statusBar()->setPalette(sbPal); statusBar()->setPalette(sbPal);
} }
// View toggle buttons in status bar
{
auto applyColors = [&](ViewTabButton* btn) {
btn->colBg = theme.background;
btn->colBgChecked = theme.backgroundAlt;
btn->colBgHover = theme.hover;
btn->colBgPressed = theme.hover.darker(130);
btn->colText = theme.text;
btn->colTextMuted = theme.textMuted;
btn->colAccent = theme.indHoverSpan;
btn->update();
};
applyColors(static_cast<ViewTabButton*>(m_btnReclass));
applyColors(static_cast<ViewTabButton*>(m_btnRendered));
}
// Resize grip (direct child of main window, not in status bar)
if (auto* w = findChild<QWidget*>("resizeGrip"))
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
// Workspace tree: text color matches menu bar // Workspace tree: text color matches menu bar
if (m_workspaceTree) { if (m_workspaceTree) {
@@ -1068,10 +1302,44 @@ void MainWindow::applyTheme(const Theme& theme) {
m_workspaceTree->setPalette(tp); m_workspaceTree->setPalette(tp);
} }
// Split pane tab widgets // Dock titlebar: restyle label + close button
for (auto& state : m_tabs) { if (m_dockTitleLabel)
for (auto& pane : state.panes) { m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
if (pane.tabWidget) applyTabWidgetStyle(pane.tabWidget); if (m_dockCloseBtn)
m_dockCloseBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Rendered C/C++ views: update lexer colors, paper, margins
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
auto* sci = pane.rendered;
if (!sci) continue;
if (auto* lexer = qobject_cast<QsciLexerCPP*>(sci->lexer())) {
lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::KeywordSet2);
lexer->setColor(theme.syntaxNumber, QsciLexerCPP::Number);
lexer->setColor(theme.syntaxString, QsciLexerCPP::DoubleQuotedString);
lexer->setColor(theme.syntaxString, QsciLexerCPP::SingleQuotedString);
lexer->setColor(theme.syntaxComment, QsciLexerCPP::Comment);
lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentLine);
lexer->setColor(theme.syntaxComment, QsciLexerCPP::CommentDoc);
lexer->setColor(theme.text, QsciLexerCPP::Default);
lexer->setColor(theme.text, QsciLexerCPP::Identifier);
lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
lexer->setColor(theme.text, QsciLexerCPP::Operator);
for (int i = 0; i <= 127; i++)
lexer->setPaper(theme.background, i);
}
sci->setPaper(theme.background);
sci->setColor(theme.text);
sci->setCaretForegroundColor(theme.text);
sci->setCaretLineBackgroundColor(theme.hover);
sci->setSelectionBackgroundColor(theme.selection);
sci->setSelectionForegroundColor(theme.text);
sci->setMarginsBackgroundColor(theme.backgroundAlt);
sci->setMarginsForegroundColor(theme.textDim);
} }
} }
} }
@@ -1156,8 +1424,13 @@ void MainWindow::setEditorFont(const QString& fontName) {
// Sync workspace tree font // Sync workspace tree font
if (m_workspaceTree) if (m_workspaceTree)
m_workspaceTree->setFont(f); m_workspaceTree->setFont(f);
// Sync dock titlebar font
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(f);
// Sync status bar font // Sync status bar font
statusBar()->setFont(f); statusBar()->setFont(f);
m_btnReclass->setFont(f);
m_btnRendered->setFont(f);
} }
RcxController* MainWindow::activeController() const { RcxController* MainWindow::activeController() const {
@@ -1268,7 +1541,13 @@ void MainWindow::setViewMode(ViewMode mode) {
pane->viewMode = mode; pane->viewMode = mode;
int idx = (mode == VM_Rendered) ? 1 : 0; int idx = (mode == VM_Rendered) ? 1 : 0;
pane->tabWidget->setCurrentIndex(idx); pane->tabWidget->setCurrentIndex(idx);
// The QTabWidget::currentChanged signal will handle updating the rendered view syncViewButtons(mode);
}
void MainWindow::syncViewButtons(ViewMode mode) {
QSignalBlocker block(m_viewBtnGroup);
if (mode == VM_Rendered) m_btnRendered->setChecked(true);
else m_btnReclass->setChecked(true);
} }
// ── Find the root-level struct ancestor for a node ── // ── Find the root-level struct ancestor for a node ──
@@ -1635,6 +1914,42 @@ void MainWindow::createWorkspaceDock() {
m_workspaceDock = new QDockWidget("Project Tree", this); m_workspaceDock = new QDockWidget("Project Tree", 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);
// Custom titlebar: label + ✕ close button (matches MDI tab style)
{
const auto& t = ThemeManager::instance().current();
auto* titleBar = new QWidget(m_workspaceDock);
auto* layout = new QHBoxLayout(titleBar);
layout->setContentsMargins(6, 2, 2, 2);
layout->setSpacing(0);
m_dockTitleLabel = new QLabel("Project Tree", titleBar);
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(t.textDim.name()));
{
QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
QFont f(fontName, 12);
f.setFixedPitch(true);
m_dockTitleLabel->setFont(f);
}
layout->addWidget(m_dockTitleLabel);
layout->addStretch();
m_dockCloseBtn = new QToolButton(titleBar);
m_dockCloseBtn->setText(QStringLiteral("\u2715"));
m_dockCloseBtn->setAutoRaise(true);
m_dockCloseBtn->setCursor(Qt::PointingHandCursor);
m_dockCloseBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
"QToolButton:hover { color: %2; }")
.arg(t.textDim.name(), t.indHoverSpan.name()));
connect(m_dockCloseBtn, &QToolButton::clicked, m_workspaceDock, &QDockWidget::close);
layout->addWidget(m_dockCloseBtn);
m_workspaceDock->setTitleBarWidget(titleBar);
}
m_workspaceTree = new QTreeView(m_workspaceDock); m_workspaceTree = new QTreeView(m_workspaceDock);
m_workspaceModel = new QStandardItemModel(this); m_workspaceModel = new QStandardItemModel(this);
@@ -1689,7 +2004,53 @@ void MainWindow::createWorkspaceDock() {
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)); QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
if (chosen == actDelete) { if (chosen == actDelete) {
tab.ctrl->removeNode(ni); QString typeName = tab.doc->tree.nodes[ni].structTypeName.isEmpty()
? tab.doc->tree.nodes[ni].name
: tab.doc->tree.nodes[ni].structTypeName;
if (typeName.isEmpty()) typeName = QStringLiteral("(unnamed)");
// Collect detailed reference info
QStringList refDetails;
for (const auto& n : tab.doc->tree.nodes) {
if (n.refId == structId) {
QString ownerName;
uint64_t pid = n.parentId;
while (pid != 0) {
int pi = tab.doc->tree.indexOfId(pid);
if (pi < 0) break;
if (tab.doc->tree.nodes[pi].parentId == 0) {
ownerName = tab.doc->tree.nodes[pi].structTypeName.isEmpty()
? tab.doc->tree.nodes[pi].name
: tab.doc->tree.nodes[pi].structTypeName;
break;
}
pid = tab.doc->tree.nodes[pi].parentId;
}
QString fieldDesc = ownerName.isEmpty()
? n.name
: QStringLiteral("%1::%2").arg(ownerName, n.name);
refDetails << QStringLiteral(" \u2022 %1 (%2)")
.arg(fieldDesc, kindToString(n.kind));
}
}
QString msg;
if (refDetails.isEmpty()) {
msg = QString("Delete '%1'?").arg(typeName);
} else {
msg = QString("Delete '%1'?\n\n"
"The following %2 field(s) reference this type "
"and will become untyped (void):\n\n%3")
.arg(typeName)
.arg(refDetails.size())
.arg(refDetails.join('\n'));
}
auto answer = QMessageBox::question(this, "Delete Type", msg,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (answer != QMessageBox::Yes) return;
tab.ctrl->deleteRootStruct(structId);
rebuildWorkspaceModel(); rebuildWorkspaceModel();
} else if (chosen && chosen == actConvert) { } else if (chosen && chosen == actConvert) {
QString newKw = kw == QStringLiteral("class") QString newKw = kw == QStringLiteral("class")
@@ -1731,6 +2092,12 @@ void MainWindow::createWorkspaceDock() {
}); });
} }
void MainWindow::rebuildAllDocs() {
m_allDocs.clear();
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
m_allDocs.append(it.value().doc);
}
void MainWindow::rebuildWorkspaceModel() { void MainWindow::rebuildWorkspaceModel() {
QVector<rcx::TabInfo> tabs; QVector<rcx::TabInfo> tabs;
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) { for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
@@ -1744,6 +2111,41 @@ void MainWindow::rebuildWorkspaceModel() {
m_workspaceTree->expandToDepth(1); m_workspaceTree->expandToDepth(1);
} }
void MainWindow::populateSourceMenu() {
m_sourceMenu->clear();
auto* ctrl = activeController();
m_sourceMenu->addAction("File", this, [this]() {
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
});
const auto& providers = ProviderRegistry::instance().providers();
for (const auto& prov : providers) {
QString name = prov.name;
m_sourceMenu->addAction(name, this, [this, name]() {
if (auto* c = activeController()) c->selectSource(name);
});
}
if (ctrl && !ctrl->savedSources().isEmpty()) {
m_sourceMenu->addSeparator();
for (int i = 0; i < ctrl->savedSources().size(); i++) {
const auto& e = ctrl->savedSources()[i];
auto* act = m_sourceMenu->addAction(
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName),
this, [this, i]() {
if (auto* c = activeController()) c->switchSource(i);
});
act->setCheckable(true);
act->setChecked(i == ctrl->activeSourceIndex());
}
m_sourceMenu->addSeparator();
m_sourceMenu->addAction("Clear All", this, [this]() {
if (auto* c = activeController()) c->clearSources();
});
}
}
void MainWindow::showPluginsDialog() { void MainWindow::showPluginsDialog() {
QDialog dialog(this); QDialog dialog(this);
dialog.setWindowTitle("Plugins"); dialog.setWindowTitle("Plugins");
@@ -1860,6 +2262,11 @@ void MainWindow::resizeEvent(QResizeEvent* event) {
m_borderOverlay->setGeometry(rect()); m_borderOverlay->setGeometry(rect());
m_borderOverlay->raise(); m_borderOverlay->raise();
} }
if (auto* w = findChild<QWidget*>("resizeGrip")) {
auto* grip = static_cast<ResizeGrip*>(w);
grip->reposition();
grip->raise();
}
} }
void MainWindow::updateBorderColor(const QColor& color) { void MainWindow::updateBorderColor(const QColor& color) {

View File

@@ -12,6 +12,8 @@
#include <QTreeView> #include <QTreeView>
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QMap> #include <QMap>
#include <QButtonGroup>
#include <QPushButton>
#include <Qsci/qsciscintilla.h> #include <Qsci/qsciscintilla.h>
namespace rcx { namespace rcx {
@@ -67,11 +69,15 @@ private:
QMdiArea* m_mdiArea; QMdiArea* m_mdiArea;
QLabel* m_statusLabel; QLabel* m_statusLabel;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr; TitleBarWidget* m_titleBar = nullptr;
QWidget* m_borderOverlay = nullptr; QWidget* m_borderOverlay = nullptr;
PluginManager m_pluginManager; PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr; McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr; QAction* m_mcpAction = nullptr;
QMenu* m_sourceMenu = nullptr;
struct SplitPane { struct SplitPane {
QTabWidget* tabWidget = nullptr; QTabWidget* tabWidget = nullptr;
@@ -89,11 +95,13 @@ private:
int activePaneIdx = 0; int activePaneIdx = 0;
}; };
QMap<QMdiSubWindow*, TabState> m_tabs; QMap<QMdiSubWindow*, TabState> m_tabs;
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
void rebuildAllDocs();
void createMenus(); void createMenus();
void createStatusBar(); void createStatusBar();
void showPluginsDialog(); void showPluginsDialog();
void populateSourceMenu();
QIcon makeIcon(const QString& svgPath); QIcon makeIcon(const QString& svgPath);
RcxController* activeController() const; RcxController* activeController() const;
@@ -111,8 +119,8 @@ private:
SplitPane createSplitPane(TabState& tab); SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme); void applyTheme(const Theme& theme);
void applyTabWidgetStyle(QTabWidget* tw);
void styleTabCloseButtons(); void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw); SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane(); SplitPane* findActiveSplitPane();
RcxEditor* activePaneEditor(); RcxEditor* activePaneEditor();
@@ -121,6 +129,8 @@ private:
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;
QToolButton* m_dockCloseBtn = nullptr;
void createWorkspaceDock(); void createWorkspaceDock();
void rebuildWorkspaceModel(); void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color); void updateBorderColor(const QColor& color);

View File

@@ -287,7 +287,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"name", "hex.read"}, {"name", "hex.read"},
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type " {"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). " "interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
"Offset is provider-relative (0-based) unless baseRelative=true."}, "Offset is tree-relative (0-based, baseAddress added automatically) "
"unless baseRelative=true (offset is absolute)."},
{"inputSchema", QJsonObject{ {"inputSchema", QJsonObject{
{"type", "object"}, {"type", "object"},
{"properties", QJsonObject{ {"properties", QJsonObject{
@@ -825,8 +826,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble()); int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int length = qMin(args.value("length").toInt(64), 4096); int length = qMin(args.value("length").toInt(64), 4096);
if (args.value("baseRelative").toBool()) if (!args.value("baseRelative").toBool())
offset -= (int64_t)tab->doc->tree.baseAddress; offset += (int64_t)tab->doc->tree.baseAddress;
if (offset < 0 || !prov->isReadable((uint64_t)offset, length)) if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
return makeTextResult("Cannot read at offset " + QString::number(offset), true); return makeTextResult("Cannot read at offset " + QString::number(offset), true);
@@ -907,8 +908,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble()); int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
QString hexStr = args.value("hexBytes").toString().remove(' '); QString hexStr = args.value("hexBytes").toString().remove(' ');
if (args.value("baseRelative").toBool()) if (!args.value("baseRelative").toBool())
offset -= (int64_t)doc->tree.baseAddress; offset += (int64_t)doc->tree.baseAddress;
if (hexStr.size() % 2 != 0) if (hexStr.size() % 2 != 0)
return makeTextResult("Hex string must have even length", true); return makeTextResult("Hex string must have even length", true);

View File

@@ -33,10 +33,10 @@ public:
// Examples: "File", "Process", "Socket" // Examples: "File", "Process", "Socket"
virtual QString kind() const { return QStringLiteral("File"); } virtual QString kind() const { return QStringLiteral("File"); }
// Base address for providers that offset reads (e.g. process memory). // Initial base address discovered by the provider (e.g. main module base).
// Used by the controller to set tree.baseAddress on first attach.
// For file/buffer providers this is always 0. // For file/buffer providers this is always 0.
virtual uint64_t base() const { return 0; } virtual uint64_t base() const { return 0; }
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
// Resolve an absolute address to a symbol name. // Resolve an absolute address to a symbol name.
// Returns empty string if no symbol is known. // Returns empty string if no symbol is known.

View File

@@ -18,7 +18,11 @@ ThemeManager::ThemeManager() {
loadUserThemes(); loadUserThemes();
QSettings settings("Reclass", "Reclass"); QSettings settings("Reclass", "Reclass");
QString fallback = m_builtIn.isEmpty() ? QString() : m_builtIn[0].name; QString fallback;
for (const auto& t : m_builtIn) {
if (t.name.contains("VS2022", Qt::CaseInsensitive)) { fallback = t.name; break; }
}
if (fallback.isEmpty() && !m_builtIn.isEmpty()) fallback = m_builtIn[0].name;
QString saved = settings.value("theme", fallback).toString(); QString saved = settings.value("theme", fallback).toString();
auto all = themes(); auto all = themes();
for (int i = 0; i < all.size(); i++) { for (int i = 0; i < all.size(); i++) {

View File

@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
if (s.endsWith('*')) { if (s.endsWith('*')) {
spec.isPointer = true; spec.isPointer = true;
s.chop(1); s.chop(1);
if (s.endsWith('*')) s.chop(1); // double pointer spec.ptrDepth = 1;
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
spec.baseName = s.trimmed(); spec.baseName = s.trimmed();
return spec; return spec;
} }
@@ -97,6 +98,12 @@ public:
int h = option.rect.height(); int h = option.rect.height();
int w = option.rect.width(); int w = option.rect.width();
// Scale metrics from font height
QFontMetrics fmMain(m_font);
int iconSz = fmMain.height(); // icon matches text height
int gutterW = fmMain.horizontalAdvance(QChar(0x25B8)) + 4;
int iconColW = iconSz + 4;
// Section: centered dim text with horizontal rules // Section: centered dim text with horizontal rules
if (isSection) { if (isSection) {
painter->setPen(t.textDim); painter->setPen(t.textDim);
@@ -133,18 +140,18 @@ public:
if (isCurrent) { if (isCurrent) {
painter->setPen(t.text); painter->setPen(t.text);
painter->setFont(m_font); painter->setFont(m_font);
painter->drawText(QRect(x, y, 10, h), Qt::AlignCenter, painter->drawText(QRect(x, y, gutterW, h), Qt::AlignCenter,
QString(QChar(0x25B8))); QString(QChar(0x25B8)));
} }
} }
x += 10; x += gutterW;
// Icon 16x16 — only for composite entries // Icon (scaled to font height) — only for composite entries
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size() bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
&& (*m_filtered)[row].entryKind == TypeEntry::Composite); && (*m_filtered)[row].entryKind == TypeEntry::Composite);
if (hasIcon) { if (hasIcon) {
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg")); static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
QPixmap pm = structIcon.pixmap(16, 16); QPixmap pm = structIcon.pixmap(iconSz, iconSz);
if (isDisabled) { if (isDisabled) {
// Paint dimmed // Paint dimmed
QPixmap dimmed(pm.size()); QPixmap dimmed(pm.size());
@@ -153,12 +160,12 @@ public:
p.setOpacity(0.35); p.setOpacity(0.35);
p.drawPixmap(0, 0, pm); p.drawPixmap(0, 0, pm);
p.end(); p.end();
painter->drawPixmap(x, y + (h - 16) / 2, dimmed); painter->drawPixmap(x, y + (h - iconSz) / 2, dimmed);
} else { } else {
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16); structIcon.paint(painter, x, y + (h - iconSz) / 2, iconSz, iconSz);
} }
} }
x += 20; x += iconColW;
// Text // Text
QColor textColor; QColor textColor;
@@ -273,14 +280,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
// Separator // Separator
{ {
auto* sep = new QFrame; m_separator = new QFrame;
sep->setFrameShape(QFrame::HLine); m_separator->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain); m_separator->setFrameShadow(QFrame::Plain);
QPalette sepPal = pal; QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, theme.border); sepPal.setColor(QPalette::WindowText, theme.border);
sep->setPalette(sepPal); m_separator->setPalette(sepPal);
sep->setFixedHeight(1); m_separator->setFixedHeight(1);
layout->addWidget(sep); layout->addWidget(m_separator);
} }
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ] // Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
@@ -341,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_arrayCountEdit->selectAll(); m_arrayCountEdit->selectAll();
} }
updateModifierPreview(); updateModifierPreview();
applyFilter(m_filterEdit->text());
}); });
connect(m_arrayCountEdit, &QLineEdit::textChanged, connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); }); this, [this]() { updateModifierPreview(); });
@@ -450,28 +456,92 @@ void TypeSelectorPopup::setFont(const QFont& font) {
delegate->setFont(font); delegate->setFont(font);
} }
void TypeSelectorPopup::applyTheme(const Theme& theme) {
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
pal.setColor(QPalette::Base, theme.background);
pal.setColor(QPalette::AlternateBase, theme.surface);
pal.setColor(QPalette::Text, theme.text);
pal.setColor(QPalette::Button, theme.button);
pal.setColor(QPalette::ButtonText, theme.text);
pal.setColor(QPalette::Highlight, theme.hover);
pal.setColor(QPalette::HighlightedText, theme.text);
setPalette(pal);
m_titleLabel->setPalette(pal);
m_filterEdit->setPalette(pal);
m_listView->setPalette(pal);
m_previewLabel->setPalette(pal);
m_arrayCountEdit->setPalette(pal);
// Separator
QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, theme.border);
m_separator->setPalette(sepPal);
// Esc button
m_escLabel->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 2px 6px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Create button
m_createBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 3px 6px; }"
"QToolButton:hover { color: %2; background: %3; }")
.arg(theme.textMuted.name(), theme.text.name(), theme.hover.name()));
// Modifier toggle buttons
QString btnStyle = QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 8px; border-radius: 3px; }"
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
"QToolButton:hover:!checked { background: %6; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name());
m_btnPlain->setStyleSheet(btnStyle);
m_btnPtr->setStyleSheet(btnStyle);
m_btnDblPtr->setStyleSheet(btnStyle);
m_btnArray->setStyleSheet(btnStyle);
// Preview label
m_previewLabel->setStyleSheet(QStringLiteral(
"QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name()));
}
void TypeSelectorPopup::setTitle(const QString& title) { void TypeSelectorPopup::setTitle(const QString& title) {
m_titleLabel->setText(title); m_titleLabel->setText(title);
} }
void TypeSelectorPopup::setMode(TypePopupMode mode) { void TypeSelectorPopup::setMode(TypePopupMode mode) {
m_mode = mode; m_mode = mode;
// Show modifier toggles for modes where type modifiers make sense
bool showMods = (mode == TypePopupMode::FieldType bool showMods = (mode == TypePopupMode::FieldType
|| mode == TypePopupMode::ArrayElement); || mode == TypePopupMode::ArrayElement);
m_modRow->setVisible(showMods); m_modRow->setVisible(showMods);
// Reset to plain when showing // Always reset to plain — prevents stale state from leaking across modes
if (showMods) { // (PointerTarget hides buttons but applyFilter still reads their state)
m_btnPlain->setChecked(true); m_btnPlain->setChecked(true);
m_arrayCountEdit->clear(); m_arrayCountEdit->clear();
m_arrayCountEdit->hide(); m_arrayCountEdit->hide();
} }
}
void TypeSelectorPopup::setCurrentNodeSize(int bytes) { void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
m_currentNodeSize = bytes; m_currentNodeSize = bytes;
} }
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
if (modId == 1) m_btnPtr->setChecked(true);
else if (modId == 2) m_btnDblPtr->setChecked(true);
else if (modId == 3) {
m_btnArray->setChecked(true);
m_arrayCountEdit->setText(QString::number(arrayCount));
m_arrayCountEdit->show();
} else {
m_btnPlain->setChecked(true);
}
}
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) { void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
m_allTypes = types; m_allTypes = types;
if (current) { if (current) {
@@ -481,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
m_currentEntry = TypeEntry{}; m_currentEntry = TypeEntry{};
m_hasCurrent = false; m_hasCurrent = false;
} }
// Reset modifier toggles // Don't reset modifier buttons here — setMode() already resets to plain,
m_btnPlain->setChecked(true); // and setModifier() may have preselected a button between setMode/setTypes.
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
m_previewLabel->hide(); m_previewLabel->hide();
m_filterEdit->clear(); m_filterEdit->clear();
@@ -498,7 +566,9 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
QString text = t.classKeyword.isEmpty() QString text = t.classKeyword.isEmpty()
? t.displayName ? t.displayName
: (t.classKeyword + QStringLiteral(" ") + t.displayName); : (t.classKeyword + QStringLiteral(" ") + t.displayName);
int w = 10 + 20 + fm.horizontalAdvance(text) + 16; int gutterW = fm.horizontalAdvance(QChar(0x25B8)) + 4;
int iconColW = fm.height() + 4;
int w = gutterW + iconColW + fm.horizontalAdvance(text) + 16;
if (w > maxTextW) maxTextW = w; if (w > maxTextW) maxTextW = w;
} }
int popupW = qBound(280, maxTextW + 24, 500); int popupW = qBound(280, maxTextW + 24, 500);
@@ -568,27 +638,26 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
QString filterBase = text.trimmed(); QString filterBase = text.trimmed();
// Hide primitives when a pointer modifier (* or **) is active // Separate primitives and composites (all types shown regardless of modifier)
int modId = m_modGroup->checkedId();
bool hideprimitives = (modId == 1 || modId == 2);
// Separate primitives and composites
QVector<TypeEntry> primitives, composites; QVector<TypeEntry> primitives, composites;
for (const auto& t : m_allTypes) { for (const auto& t : m_allTypes) {
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections if (t.entryKind == TypeEntry::Section) continue;
bool matchesFilter = filterBase.isEmpty() bool matchesFilter = filterBase.isEmpty()
|| t.displayName.contains(filterBase, Qt::CaseInsensitive) || t.displayName.contains(filterBase, Qt::CaseInsensitive)
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive); || t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
if (!matchesFilter) continue; if (!matchesFilter) continue;
if (t.entryKind == TypeEntry::Primitive) { if (t.entryKind == TypeEntry::Primitive)
if (!hideprimitives)
primitives.append(t); primitives.append(t);
} else if (t.entryKind == TypeEntry::Composite) else if (t.entryKind == TypeEntry::Composite)
composites.append(t); composites.append(t);
} }
// For non-Root modes, sort primitives: same-size first, then rest auto alphabetical = [](const TypeEntry& a, const TypeEntry& b) {
return a.displayName.compare(b.displayName, Qt::CaseInsensitive) < 0;
};
// For non-Root modes, sort primitives: same-size first, then rest — alphabetical within each group
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) { if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
QVector<TypeEntry> sameSize, other; QVector<TypeEntry> sameSize, other;
for (const auto& p : primitives) { for (const auto& p : primitives) {
@@ -597,7 +666,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
else else
other.append(p); other.append(p);
} }
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
std::sort(other.begin(), other.end(), alphabetical);
primitives = sameSize + other; primitives = sameSize + other;
} else {
std::sort(primitives.begin(), primitives.end(), alphabetical);
} }
// Helper lambdas for appending sections // Helper lambdas for appending sections

View File

@@ -16,6 +16,8 @@ class QWidget;
namespace rcx { namespace rcx {
struct Theme;
// ── Popup mode ── // ── Popup mode ──
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget }; enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
@@ -38,6 +40,7 @@ struct TypeEntry {
struct TypeSpec { struct TypeSpec {
QString baseName; QString baseName;
bool isPointer = false; bool isPointer = false;
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
int arrayCount = 0; // 0 = not array int arrayCount = 0; // 0 = not array
}; };
@@ -53,7 +56,9 @@ public:
void setFont(const QFont& font); void setFont(const QFont& font);
void setTitle(const QString& title); void setTitle(const QString& title);
void setMode(TypePopupMode mode); void setMode(TypePopupMode mode);
void applyTheme(const Theme& theme);
void setCurrentNodeSize(int bytes); void setCurrentNodeSize(int bytes);
void setModifier(int modId, int arrayCount = 0);
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr); void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
void popup(const QPoint& globalPos); void popup(const QPoint& globalPos);
@@ -77,6 +82,7 @@ private:
QLabel* m_previewLabel = nullptr; QLabel* m_previewLabel = nullptr;
QListView* m_listView = nullptr; QListView* m_listView = nullptr;
QStringListModel* m_model = nullptr; QStringListModel* m_model = nullptr;
QFrame* m_separator = nullptr;
// Modifier toggles // Modifier toggles
QWidget* m_modRow = nullptr; QWidget* m_modRow = nullptr;

View File

@@ -1,185 +0,0 @@
/**
* test_com_security.cpp — DebugConnect transport diagnostic
*
* Tests EVERY transport to find what works from MinGW:
* 1. TCP to WinDbg .server (port 5055)
* 2. Named pipe to WinDbg .server
* 3. TCP with various COM security configs
* 4. DebugCreate local (baseline)
*
* SETUP: In WinDbg, run BOTH of these:
* .server tcp:port=5055
* .server npipe:pipe=reclass
*
* Then run this test.
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#include <windows.h>
#include <objbase.h>
#include <initguid.h>
#include <dbgeng.h>
#endif
#ifdef _WIN32
static void try_connect(const char* label, const char* connStr)
{
printf(" %-40s → ", label);
fflush(stdout);
IDebugClient* client = nullptr;
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
if (SUCCEEDED(hr) && client) {
printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr);
// Try to get data spaces and read something
IDebugDataSpaces* ds = nullptr;
IDebugSymbols* sym = nullptr;
IDebugControl* ctrl = nullptr;
client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
if (ctrl) {
HRESULT hrWait = ctrl->WaitForEvent(0, 5000);
printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait);
}
if (sym) {
ULONG numMods = 0, numUnloaded = 0;
sym->GetNumberModules(&numMods, &numUnloaded);
printf(" Modules: %lu loaded\n", numMods);
if (numMods > 0 && ds) {
ULONG64 base = 0;
sym->GetModuleByIndex(0, &base);
unsigned char buf[2] = {};
ULONG got = 0;
ds->ReadVirtual(base, buf, 2, &got);
printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n",
(unsigned long long)base, got, buf[0], buf[1]);
}
}
if (sym) sym->Release();
if (ds) ds->Release();
if (ctrl) ctrl->Release();
client->Release();
} else {
char buf[256] = {};
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr);
for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p)
*p = '\0';
printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf);
}
}
#endif
int main()
{
#ifdef _WIN32
char hostname[256] = {};
DWORD hsize = sizeof(hostname);
GetComputerNameA(hostname, &hsize);
printf("=== DebugConnect Transport Diagnostic ===\n");
printf("Machine: %s\n\n", hostname);
// ── Baseline: DebugCreate (local) ──
printf("[1] DebugCreate (local, no network)\n");
{
IDebugClient* client = nullptr;
HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client);
printf(" DebugCreate: %s (hr=0x%08lX)\n\n",
SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr);
if (client) client->Release();
}
// ── TCP variants ──
printf("[2] TCP connections (need: .server tcp:port=5055)\n");
try_connect("tcp:Port=5055,Server=localhost",
"tcp:Port=5055,Server=localhost");
try_connect("tcp:Port=5055,Server=127.0.0.1",
"tcp:Port=5055,Server=127.0.0.1");
{
char conn[512];
snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname);
try_connect(conn, conn);
}
printf("\n");
// ── Named pipe variants ──
printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n");
try_connect("npipe:Pipe=reclass,Server=localhost",
"npipe:Pipe=reclass,Server=localhost");
{
char conn[512];
snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname);
try_connect(conn, conn);
}
try_connect("npipe:Pipe=reclass",
"npipe:Pipe=reclass");
printf("\n");
// ── TCP with COM security ──
printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n");
{
// This runs in-process so CoInitialize affects subsequent calls
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
CoInitializeSecurity(
nullptr, -1, nullptr, nullptr,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
nullptr, EOAC_NONE, nullptr);
try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)",
"tcp:Port=5055,Server=localhost");
try_connect("npipe:Pipe=reclass (MTA+SEC)",
"npipe:Pipe=reclass,Server=localhost");
CoUninitialize();
}
printf("\n");
// ── Check if dbgeng.dll is the system one ──
printf("[5] DbgEng DLL info\n");
{
HMODULE hmod = GetModuleHandleA("dbgeng.dll");
if (hmod) {
char path[MAX_PATH] = {};
GetModuleFileNameA(hmod, path, MAX_PATH);
printf(" dbgeng.dll loaded from: %s\n", path);
// Get version
DWORD verSize = GetFileVersionInfoSizeA(path, nullptr);
if (verSize > 0) {
auto* verData = (char*)malloc(verSize);
if (GetFileVersionInfoA(path, 0, verSize, verData)) {
VS_FIXEDFILEINFO* fileInfo = nullptr;
UINT len = 0;
if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) {
printf(" Version: %d.%d.%d.%d\n",
HIWORD(fileInfo->dwFileVersionMS),
LOWORD(fileInfo->dwFileVersionMS),
HIWORD(fileInfo->dwFileVersionLS),
LOWORD(fileInfo->dwFileVersionLS));
}
}
free(verData);
}
} else {
printf(" dbgeng.dll not loaded yet\n");
}
}
printf("\n=== Done ===\n");
return 0;
#else
printf("Windows only.\n");
return 0;
#endif
}

View File

@@ -1017,7 +1017,7 @@ private slots:
void testPrimitiveArrayElements() { void testPrimitiveArrayElements() {
// Expanded primitive array should synthesize element lines dynamically // Expanded primitive array should synthesize element lines dynamically
NodeTree tree; NodeTree tree;
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
@@ -1934,7 +1934,7 @@ private slots:
void testTextIsNonEmpty() { void testTextIsNonEmpty() {
// Verify composed text is actually generated (not empty) // Verify composed text is actually generated (not empty)
NodeTree tree; NodeTree tree;
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;

View File

@@ -8,7 +8,7 @@
using namespace rcx; using namespace rcx;
static void buildTree(NodeTree& tree) { static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
@@ -394,6 +394,65 @@ private slots:
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(countNodes(), before); QCOMPARE(countNodes(), before);
} }
// ── Change to Ptr* creates class and sets refId ──
void testChangeToPtrStarCreatesClassAndSetsRef() {
// Add a Hex64 node to the root struct
uint64_t rootId = m_doc->tree.nodes[0].id;
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField");
QApplication::processEvents();
int ptrIdx = findNode("ptrField");
QVERIFY(ptrIdx >= 0);
uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id;
int before = countNodes();
// Convert to typed pointer
m_ctrl->convertToTypedPointer(ptrNodeId);
QApplication::processEvents();
// Re-find after tree mutation
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
// Verify: node kind changed to Pointer64
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64);
// Verify: node.refId != 0
uint64_t refId = m_doc->tree.nodes[ptrIdx].refId;
QVERIFY(refId != 0);
// Verify: a new Struct node exists with the refId as its id
int structIdx = m_doc->tree.indexOfId(refId);
QVERIFY(structIdx >= 0);
QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct);
// Verify: the new struct has children (Hex64 fields)
auto children = m_doc->tree.childrenOf(refId);
QVERIFY(children.size() == 16);
for (int ci : children)
QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64);
// Verify: total nodes increased by 1 struct + 16 children = 17
QCOMPARE(countNodes(), before + 17);
// Verify: undo restores the original Hex64 kind and refId==0
m_doc->undoStack.undo();
QApplication::processEvents();
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64);
QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0);
QCOMPARE(countNodes(), before);
}
}; };
QTEST_MAIN(TestContextMenu) QTEST_MAIN(TestContextMenu)

View File

@@ -22,7 +22,6 @@ public:
} }
int size() const override { return m_data.size(); } int size() const override { return m_data.size(); }
uint64_t base() const override { return m_base; } uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
bool isLive() const override { return true; } bool isLive() const override { return true; }
QString name() const override { return QStringLiteral("test"); } QString name() const override { return QStringLiteral("test"); }
QString kind() const override { return QStringLiteral("Process"); } QString kind() const override { return QStringLiteral("Process"); }
@@ -31,7 +30,7 @@ public:
// Small tree: one root struct with a few typed fields at known offsets. // Small tree: one root struct with a few typed fields at known offsets.
// Keeps tests fast and deterministic (no giant PEB tree). // Keeps tests fast and deterministic (no giant PEB tree).
static void buildSmallTree(NodeTree& tree) { static void buildSmallTree(NodeTree& tree) {
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;
@@ -405,7 +404,8 @@ private slots:
// ── Test: source switch preserves existing base address ── // ── Test: source switch preserves existing base address ──
void testSourceSwitchPreservesBase() { void testSourceSwitchPreservesBase() {
// Document already has baseAddress = 0x1000 from buildSmallTree() // Set a non-zero baseAddress to simulate a loaded .rcx file
m_doc->tree.baseAddress = 0x1000;
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000); QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// Simulate attaching a new provider whose base differs (e.g. 0x400000) // Simulate attaching a new provider whose base differs (e.g. 0x400000)
@@ -414,16 +414,14 @@ private slots:
QCOMPARE(newBase, (uint64_t)0x400000); QCOMPARE(newBase, (uint64_t)0x400000);
m_doc->provider = prov; m_doc->provider = prov;
// This is the controller logic under test: // Controller logic: keep existing baseAddress when non-zero
if (m_doc->tree.baseAddress == 0) if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase; m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// baseAddress must stay at the original value // baseAddress must stay at the original value
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000); QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// provider base must be synced to match // provider base is unchanged (no setBase sync) — provider reports its own initial base
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000); QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
} }
// ── Test: source switch on fresh doc uses provider default ── // ── Test: source switch on fresh doc uses provider default ──
@@ -437,12 +435,9 @@ private slots:
m_doc->provider = prov; m_doc->provider = prov;
if (m_doc->tree.baseAddress == 0) if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase; m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// Fresh doc should adopt the provider's default base // Fresh doc should adopt the provider's default base
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000); QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
} }
// ── Test: toggleCollapse + undo ── // ── Test: toggleCollapse + undo ──

View File

@@ -133,19 +133,18 @@ private slots:
// ────────────────────────────────────────────────── // ──────────────────────────────────────────────────
void testVTableDisasm_composedAddress() { void testVTableDisasm_composedAddress() {
// Memory layout (provider-relative, i.e. offset from baseAddress): // Memory layout (absolute addresses, baseAddress = 0):
// //
// [0x0000] Root "Obj" struct // [0x0000] Root "Obj" struct
// +0x00: Pointer64 __vptr => points to 0xBASE+0x100 (vtable) // +0x00: Pointer64 __vptr => points to 0x100 (vtable)
// //
// [0x0100] VTable (expanded via pointer deref) // [0x0100] VTable (expanded via pointer deref)
// +0x00: func ptr 0 => value 0xBASE+0x200 (func0 code) // +0x00: func ptr 0 => value 0x200 (func0 code)
// +0x08: func ptr 1 => value 0xBASE+0x300 (func1 code) // +0x08: func ptr 1 => value 0x300 (func1 code)
// //
// [0x0200] func0 code: push rbp; ret // [0x0200] func0 code: push rbp; ret
// [0x0300] func1 code: xor eax, eax; ret // [0x0300] func1 code: xor eax, eax; ret
// //
const uint64_t kBase = 0x7FF600000000ULL;
// Build a 4KB buffer // Build a 4KB buffer
QByteArray mem(4096, '\0'); QByteArray mem(4096, '\0');
@@ -153,12 +152,12 @@ private slots:
memcpy(mem.data() + off, &val, 8); memcpy(mem.data() + off, &val, 8);
}; };
// Root object at offset 0: __vptr points to vtable at kBase + 0x100 // Root object at offset 0: __vptr points to vtable at 0x100
w64(0x00, kBase + 0x100); w64(0x00, 0x100);
// VTable at offset 0x100: two function pointers // VTable at offset 0x100: two function pointers
w64(0x100, kBase + 0x200); // slot 0 -> func0 w64(0x100, 0x200); // slot 0 -> func0
w64(0x108, kBase + 0x300); // slot 1 -> func1 w64(0x108, 0x300); // slot 1 -> func1
// func0 at offset 0x200: push rbp; ret // func0 at offset 0x200: push rbp; ret
mem[0x200] = '\x55'; mem[0x200] = '\x55';
@@ -173,7 +172,7 @@ private slots:
// Build node tree // Build node tree
NodeTree tree; NodeTree tree;
tree.baseAddress = kBase; tree.baseAddress = 0;
// Root struct "Obj" // Root struct "Obj"
Node root; Node root;
@@ -227,8 +226,8 @@ private slots:
for (int i = 0; i < result.meta.size(); i++) { for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i]; const LineMeta& lm = result.meta[i];
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) { if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
// Only include the pointer-expanded ones (near vtable at kBase+0x100) // Only include the pointer-expanded ones (near vtable at 0x100)
if (lm.offsetAddr >= kBase + 0x100 && lm.offsetAddr < kBase + 0x200) { if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
int nodeIdx = lm.nodeIdx; int nodeIdx = lm.nodeIdx;
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind, funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()}); nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
@@ -239,29 +238,29 @@ private slots:
QCOMPARE(funcPtrs.size(), 2); QCOMPARE(funcPtrs.size(), 2);
// Verify composed addresses point to the vtable, NOT to the root struct // Verify composed addresses point to the vtable, NOT to the root struct
// func0 should be at kBase + 0x100 (vtable + 0) // func0 should be at 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, kBase + 0x100); QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
// func1 should be at kBase + 0x108 (vtable + 8) // func1 should be at 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, kBase + 0x108); QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
// Now simulate what the hover code should do: // Now simulate what the hover code should do:
// Read the function pointer VALUE from the correct provider address // Read the function pointer VALUE from the correct provider address
for (const auto& fp : funcPtrs) { for (const auto& fp : funcPtrs) {
// Provider-relative address = offsetAddr - baseAddress // Provider reads at absolute address directly
uint64_t provAddr = fp.offsetAddr - kBase; uint64_t provAddr = fp.offsetAddr;
// Read the pointer value (the function address) // Read the pointer value (the function address)
uint64_t ptrVal = prov.readU64(provAddr); uint64_t ptrVal = prov.readU64(provAddr);
// Verify we got the right pointer values // Verify we got the right pointer values
if (fp.name == "func0") { if (fp.name == "func0") {
QCOMPARE(ptrVal, kBase + 0x200); QCOMPARE(ptrVal, (uint64_t)0x200);
} else { } else {
QCOMPARE(ptrVal, kBase + 0x300); QCOMPARE(ptrVal, (uint64_t)0x300);
} }
// Convert pointer value to provider-relative for reading code bytes // Read code bytes at the pointer target (absolute address)
uint64_t codeProvAddr = ptrVal - kBase; uint64_t codeProvAddr = ptrVal;
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128); QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
// Disassemble and verify // Disassemble and verify
@@ -275,14 +274,14 @@ private slots:
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp")); QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret")); QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
// Verify address in output matches the real function address // Verify address in output matches the real function address
QVERIFY2(lines[0].startsWith("00007ff600000200"), QVERIFY2(lines[0].contains("200"),
qPrintable("func0 addr wrong: " + lines[0])); qPrintable("func0 addr wrong: " + lines[0]));
} else { } else {
// Should decode: xor eax, eax; ret // Should decode: xor eax, eax; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_))); QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax")); QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret")); QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
QVERIFY2(lines[0].startsWith("00007ff600000300"), QVERIFY2(lines[0].contains("300"),
qPrintable("func1 addr wrong: " + lines[0])); qPrintable("func1 addr wrong: " + lines[0]));
} }
} }
@@ -292,26 +291,25 @@ private slots:
// inside the ROOT struct, not the vtable. // inside the ROOT struct, not the vtable.
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
// wrongVal0 = kBase + 0x100 (the vptr itself, NOT a function address) // wrongVal0 = 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, kBase + 0x100); QCOMPARE(wrongVal0, (uint64_t)0x100);
// This is the vtable address, not a function — disassembling it would be wrong // This is the vtable address, not a function — disassembling it would be wrong
QVERIFY2(wrongVal0 != kBase + 0x200, QVERIFY2(wrongVal0 != (uint64_t)0x200,
"node.offset reads the vptr, not the function pointer"); "node.offset reads the vptr, not the function pointer");
QVERIFY2(wrongVal1 != kBase + 0x300, QVERIFY2(wrongVal1 != (uint64_t)0x300,
"node.offset=8 reads past vptr, not the second function pointer"); "node.offset=8 reads past vptr, not the second function pointer");
} }
void testVTableDisasm_wrongAddressGivesWrongCode() { void testVTableDisasm_wrongAddressGivesWrongCode() {
// Demonstrate that using node.offset instead of composed address // Demonstrate that using node.offset instead of composed address
// gives completely wrong disassembly results // gives completely wrong disassembly results
const uint64_t kBase = 0x10000;
QByteArray mem(1024, '\0'); QByteArray mem(1024, '\0');
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); }; auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
// Root at 0: vptr -> 0x80 // Root at 0: vptr -> 0x80
w64(0x00, kBase + 0x80); w64(0x00, (uint64_t)0x80);
// VTable at 0x80: one func ptr -> 0x100 // VTable at 0x80: one func ptr -> 0x100
w64(0x80, kBase + 0x100); w64(0x80, (uint64_t)0x100);
// Code at 0x100: sub rsp, 0x28; nop; ret // Code at 0x100: sub rsp, 0x28; nop; ret
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec'; mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3'; mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
@@ -320,15 +318,15 @@ private slots:
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr) // WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
uint64_t wrongPtrVal = prov.readU64(0); uint64_t wrongPtrVal = prov.readU64(0);
QCOMPARE(wrongPtrVal, kBase + 0x80); // This is the vtable addr, not a function! QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
// RIGHT: read from composed address (vtable + 0) // RIGHT: read from composed address (vtable + 0)
uint64_t rightPtrVal = prov.readU64(0x80); uint64_t rightPtrVal = prov.readU64(0x80);
QCOMPARE(rightPtrVal, kBase + 0x100); // This IS the function address QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
// Disassemble the RIGHT target // Disassemble the RIGHT target
QByteArray rightCode = prov.readBytes(0x100, 128); QByteArray rightCode = prov.readBytes(0x100, 128);
QString rightAsm = disassemble(rightCode, kBase + 0x100, 64, 128); QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
QStringList rightLines = rightAsm.split('\n'); QStringList rightLines = rightAsm.split('\n');
QVERIFY(rightLines.size() >= 3); QVERIFY(rightLines.size() >= 3);
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28")); QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
@@ -337,7 +335,7 @@ private slots:
// Disassemble the WRONG target (vtable data, not code!) // Disassemble the WRONG target (vtable data, not code!)
QByteArray wrongCode = prov.readBytes(0x80, 128); QByteArray wrongCode = prov.readBytes(0x80, 128);
QString wrongAsm = disassemble(wrongCode, kBase + 0x80, 64, 128); QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
// The wrong bytes are the vtable entries (pointer values), // The wrong bytes are the vtable entries (pointer values),
// which decode as garbage instructions, not sub/nop/ret // which decode as garbage instructions, not sub/nop/ret
QVERIFY2(!wrongAsm.contains("sub rsp"), QVERIFY2(!wrongAsm.contains("sub rsp"),
@@ -348,9 +346,9 @@ private slots:
// Full simulation of the hover flow as implemented in editor.cpp: // Full simulation of the hover flow as implemented in editor.cpp:
// //
// 1. Compose the tree to get LineMeta with correct offsetAddr // 1. Compose the tree to get LineMeta with correct offsetAddr
// 2. For each FuncPtr64 line, read pointer value from snapshot/provider // 2. For each FuncPtr64 line, read pointer value from provider
// using lm.offsetAddr - baseAddress (composed address) // using lm.offsetAddr (absolute address)
// 3. Read code bytes from the REAL provider using ptrVal - baseAddress // 3. Read code bytes from the REAL provider using ptrVal directly
// (the real provider can read any process address; snapshot cannot) // (the real provider can read any process address; snapshot cannot)
// 4. Disassemble the code bytes // 4. Disassemble the code bytes
// //
@@ -358,28 +356,25 @@ private slots:
// the snapshot), step 3 reads from arbitrary code addresses (needs // the snapshot), step 3 reads from arbitrary code addresses (needs
// the real provider, not snapshot). // the real provider, not snapshot).
const uint64_t kBase = 0x7FF600000000ULL;
QByteArray mem(8192, '\0'); QByteArray mem(8192, '\0');
auto w64 = [&](int off, uint64_t val) { auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8); memcpy(mem.data() + off, &val, 8);
}; };
// Layout: // Layout:
// [0x000] Root struct: __vptr -> vtable at kBase + 0x100 // [0x000] Root struct: __vptr -> vtable at 0x100
// [0x100] VTable: func0 -> kBase + 0x1000, func1 -> kBase + 0x1800 // [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret // [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
// [0x1800] func1 code: xor eax, eax; ret // [0x1800] func1 code: xor eax, eax; ret
w64(0x000, kBase + 0x100); // __vptr w64(0x000, (uint64_t)0x100); // __vptr
w64(0x100, kBase + 0x1000); // vtable[0] w64(0x100, (uint64_t)0x1000); // vtable[0]
w64(0x108, kBase + 0x1800); // vtable[1] w64(0x108, (uint64_t)0x1800); // vtable[1]
// func0 code // func0 code
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9); memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
// func1 code // func1 code
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3); memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
// This provider represents the real process memory. // This provider represents the real process memory.
// In production, this is the ProcessMemoryProvider that reads via
// ReadProcessMemory at m_base + addr.
BufferProvider realProv(mem); BufferProvider realProv(mem);
// Build a snapshot that only contains tree-data pages (like the // Build a snapshot that only contains tree-data pages (like the
@@ -392,7 +387,7 @@ private slots:
// Build node tree // Build node tree
NodeTree tree; NodeTree tree;
tree.baseAddress = kBase; tree.baseAddress = 0;
Node root; root.kind = NodeKind::Struct; root.name = "Obj"; Node root; root.kind = NodeKind::Struct; root.name = "Obj";
root.parentId = 0; root.offset = 0; root.parentId = 0; root.offset = 0;
@@ -423,11 +418,11 @@ private slots:
const LineMeta& lm = result.meta[i]; const LineMeta& lm = result.meta[i];
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field) if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
continue; continue;
if (lm.offsetAddr < kBase + 0x100 || lm.offsetAddr >= kBase + 0x200) if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
continue; // skip standalone VTable definition entries continue; // skip standalone VTable definition entries
// --- Hover step 1: read pointer value from snapshot --- // --- Hover step 1: read pointer value from snapshot ---
uint64_t provAddr = lm.offsetAddr - tree.baseAddress; uint64_t provAddr = lm.offsetAddr;
// The snapshot has this data (vtable pages are in it) // The snapshot has this data (vtable pages are in it)
QVERIFY2(snapProv.isReadable(provAddr, 8), QVERIFY2(snapProv.isReadable(provAddr, 8),
qPrintable(QString("Snapshot should have vtable page at %1") qPrintable(QString("Snapshot should have vtable page at %1")
@@ -437,7 +432,7 @@ private slots:
// --- Hover step 2: read code from REAL provider --- // --- Hover step 2: read code from REAL provider ---
// The snapshot does NOT have the code pages: // The snapshot does NOT have the code pages:
uint64_t codeAddr = ptrVal - tree.baseAddress; uint64_t codeAddr = ptrVal;
QVERIFY2(!snapProv.isReadable(codeAddr, 1), QVERIFY2(!snapProv.isReadable(codeAddr, 1),
"Snapshot should NOT have function code pages"); "Snapshot should NOT have function code pages");
// But the real provider does: // But the real provider does:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,246 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <QDir>
#include <QFile>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "core.h"
#include "providers/null_provider.h"
#include "providers/buffer_provider.h"
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestClass";
root.name = "TestClass";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f;
f.kind = NodeKind::Hex64;
f.name = "field_00";
f.parentId = rootId;
f.offset = 0;
tree.addNode(f);
}
class TestSourceManagement : public QObject {
Q_OBJECT
private:
RcxDocument* m_doc = nullptr;
RcxController* m_ctrl = nullptr;
QSplitter* m_splitter = nullptr;
// Helper: write a temp binary file and return its path
QString writeTempFile(const QString& name, const QByteArray& data) {
QString path = QDir::tempPath() + "/" + name;
QFile f(path);
f.open(QIODevice::WriteOnly);
f.write(data);
f.close();
return path;
}
// Helper: directly add a file source entry (bypasses QFileDialog)
void addFileSource(const QString& path, const QString& displayName) {
m_doc->loadData(path);
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = displayName;
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
// Access saved sources through selectSource's internal mechanism
// We manually add since selectSource("File") opens a dialog
m_ctrl->document()->provider = std::make_shared<BufferProvider>(
QFile(path).readAll().isEmpty() ? QByteArray(64, '\0') : QByteArray(64, '\0'));
// Use the test accessor pattern from controller
}
private slots:
void init() {
m_doc = new RcxDocument();
buildTree(m_doc->tree);
m_splitter = new QSplitter();
m_ctrl = new RcxController(m_doc, nullptr);
m_ctrl->addSplitEditor(m_splitter);
m_splitter->resize(800, 600);
m_splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
QApplication::processEvents();
}
void cleanup() {
delete m_ctrl; m_ctrl = nullptr;
delete m_splitter; m_splitter = nullptr;
delete m_doc; m_doc = nullptr;
}
// ── Initial state: NullProvider, no saved sources ──
void testInitialProviderIsNull() {
QVERIFY(m_doc->provider != nullptr);
QCOMPARE(m_doc->provider->size(), 0);
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── Loading binary data creates a valid provider ──
void testLoadDataCreatesValidProvider() {
QByteArray data(128, '\xAB');
m_doc->loadData(data);
QApplication::processEvents();
QVERIFY(m_doc->provider->isValid());
QCOMPARE(m_doc->provider->size(), 128);
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
}
// ── clearSources resets to NullProvider ──
void testClearSourcesResetsToNull() {
// Load some data first so provider is valid
QByteArray data(64, '\xFF');
m_doc->loadData(data);
QApplication::processEvents();
QVERIFY(m_doc->provider->isValid());
m_ctrl->clearSources();
QApplication::processEvents();
// Provider should be NullProvider
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_doc->provider->size(), 0);
// Saved sources should be empty
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── clearSources clears value history ──
void testClearSourcesClearsValueHistory() {
// The value history is cleared via resetSnapshot inside clearSources
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_ctrl->valueHistory().isEmpty());
}
// ── clearSources clears dataPath ──
void testClearSourcesClearsDataPath() {
QString path = writeTempFile("rcx_test_src.bin", QByteArray(64, '\xCC'));
m_doc->loadData(path);
QVERIFY(!m_doc->dataPath.isEmpty());
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_doc->dataPath.isEmpty());
QFile::remove(path);
}
// ── selectSource("#clear") calls clearSources ──
void testSelectSourceClearCommand() {
QByteArray data(64, '\xFF');
m_doc->loadData(data);
QVERIFY(m_doc->provider->isValid());
m_ctrl->selectSource(QStringLiteral("#clear"));
QApplication::processEvents();
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── clearSources then refresh still works (compose doesn't crash) ──
void testClearSourcesThenRefreshWorks() {
m_ctrl->clearSources();
QApplication::processEvents();
// refresh() is called internally by clearSources; verify it didn't crash
// and the editor still has content (the tree structure is intact)
auto* editor = m_ctrl->editors().first();
QVERIFY(editor != nullptr);
}
// ── Multiple clearSources calls are safe (idempotent) ──
void testMultipleClearSourcesIdempotent() {
m_ctrl->clearSources();
m_ctrl->clearSources();
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── switchToSavedSource with invalid index is no-op ──
void testSwitchInvalidIndexNoOp() {
m_ctrl->switchSource(-1);
m_ctrl->switchSource(999);
QApplication::processEvents();
// Should still be in initial state
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── Provider read fails after clear (all zeros) ──
void testProviderReadFailsAfterClear() {
QByteArray data(64, '\xAB');
m_doc->loadData(data);
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
m_ctrl->clearSources();
QApplication::processEvents();
// NullProvider: read returns false, readU8 returns 0
uint8_t buf = 0xFF;
QVERIFY(!m_doc->provider->read(0, &buf, 1));
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0);
}
// ── clearSources resets snapshot state ──
void testClearSourcesResetsSnapshot() {
QByteArray data(64, '\x00');
m_doc->loadData(data);
QApplication::processEvents();
m_ctrl->clearSources();
QApplication::processEvents();
// After clear, the value history should be empty (resetSnapshot was called)
QVERIFY(m_ctrl->valueHistory().isEmpty());
}
// ── NullProvider name is empty (triggers "source" placeholder in command row) ──
void testNullProviderNameEmpty() {
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_doc->provider->name().isEmpty());
}
};
QTEST_MAIN(TestSourceManagement)
#include "test_source_management.moc"

View File

@@ -5,6 +5,7 @@
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QToolButton> #include <QToolButton>
#include <QButtonGroup>
#include <QLineEdit> #include <QLineEdit>
#include <QListView> #include <QListView>
#include <QStringListModel> #include <QStringListModel>
@@ -21,7 +22,7 @@ Q_DECLARE_METATYPE(rcx::TypeEntry)
using namespace rcx; using namespace rcx;
static void buildTwoRootTree(NodeTree& tree) { static void buildTwoRootTree(NodeTree& tree) {
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node a; Node a;
a.kind = NodeKind::Struct; a.kind = NodeKind::Struct;
@@ -498,6 +499,7 @@ private slots:
TypeSpec spec = parseTypeSpec("Ball*"); TypeSpec spec = parseTypeSpec("Ball*");
QCOMPARE(spec.baseName, QString("Ball")); QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer); QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 1);
QCOMPARE(spec.arrayCount, 0); QCOMPARE(spec.arrayCount, 0);
} }
@@ -505,6 +507,7 @@ private slots:
TypeSpec spec = parseTypeSpec("Ball**"); TypeSpec spec = parseTypeSpec("Ball**");
QCOMPARE(spec.baseName, QString("Ball")); QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer); QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 2);
} }
void testParseTypeSpecEmpty() { void testParseTypeSpecEmpty() {
@@ -793,6 +796,675 @@ private slots:
delete splitter; delete splitter;
delete doc; delete doc;
} }
// ── Test: SVG icon and gutter scale with font size ──
void testDelegateIconScalesWithFont() {
// Create a popup and set two different font sizes.
// The delegate sizeHint row height should scale with font.
TypeSelectorPopup popup;
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = QStringLiteral("int32_t");
TypeEntry comp;
comp.entryKind = TypeEntry::Composite;
comp.structId = 100;
comp.displayName = QStringLiteral("TestStruct");
comp.classKeyword = QStringLiteral("struct");
// Small font
QFont small(QStringLiteral("Consolas"), 9);
popup.setFont(small);
popup.setTypes({prim, comp});
popup.popup(QPoint(-9999, -9999)); // offscreen
QApplication::processEvents();
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
auto* delegate = listView->itemDelegate();
QVERIFY(delegate);
// Find first non-section row for consistent measurement
int dataRow = -1;
for (int i = 0; i < listView->model()->rowCount(); i++) {
QSize h = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(i, 0));
// Non-section rows are taller (font.height + 8 vs + 2)
if (h.height() > QFontMetrics(small).height() + 4) { dataRow = i; break; }
}
QVERIFY2(dataRow >= 0, "Should find a non-section row");
QSize smallHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
popup.hide();
// Large font (simulates zoomed editor)
QFont large(QStringLiteral("Consolas"), 18);
popup.setFont(large);
popup.setTypes({prim, comp});
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
QSize largeHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
popup.hide();
// Large font should produce taller rows than small font
QVERIFY2(largeHint.height() > smallHint.height(),
qPrintable(QString("Large hint %1 should be > small hint %2")
.arg(largeHint.height()).arg(smallHint.height())));
// The ratio should roughly match the font size ratio (18/9 = 2x)
double ratio = double(largeHint.height()) / double(smallHint.height());
QVERIFY2(ratio > 1.4, qPrintable(QString("Row height ratio %1 should be > 1.4").arg(ratio)));
}
void testPopupWidthScalesWithFont() {
TypeSelectorPopup popup;
TypeEntry comp;
comp.entryKind = TypeEntry::Composite;
comp.structId = 100;
comp.displayName = QStringLiteral("MyLongStructName");
comp.classKeyword = QStringLiteral("struct");
popup.setTypes({comp});
// Small font
QFont small(QStringLiteral("Consolas"), 9);
popup.setFont(small);
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
int smallW = popup.width();
popup.hide();
// Large font
QFont large(QStringLiteral("Consolas"), 18);
popup.setFont(large);
popup.setTypes({comp});
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
int largeW = popup.width();
popup.hide();
// Popup with larger font should be wider
QVERIFY2(largeW > smallW,
qPrintable(QString("Large popup width %1 should be > small %2")
.arg(largeW).arg(smallW)));
}
// ── Test: popup updates colors when theme changes ──
void testPopupUpdatesOnThemeChange() {
auto& tm = ThemeManager::instance();
int origIdx = tm.currentIndex();
// Ensure at least two themes exist
QVERIFY2(tm.themes().size() >= 2,
"Need at least 2 themes to test theme switching");
// Create popup with current theme
TypeSelectorPopup popup;
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = QStringLiteral("int32_t");
popup.setTypes({prim});
QColor bgBefore = popup.palette().color(QPalette::Window);
// Switch to a different theme
int otherIdx = (origIdx == 0) ? 1 : 0;
tm.setCurrent(otherIdx);
QApplication::processEvents();
// The popup should have applyTheme connected to themeChanged
popup.applyTheme(tm.current());
QColor bgAfter = popup.palette().color(QPalette::Window);
// If the two themes have different background colors, verify the change
// (some themes may coincidentally share colors, so we just verify the
// method doesn't crash and the palette is set to the new theme's color)
QCOMPARE(bgAfter, tm.current().backgroundAlt);
// Also verify child widgets got updated
auto* filterEdit = popup.findChild<QLineEdit*>();
QVERIFY(filterEdit);
QCOMPARE(filterEdit->palette().color(QPalette::Base),
tm.current().background);
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
QCOMPARE(listView->palette().color(QPalette::Base),
tm.current().background);
// Restore original theme
tm.setCurrent(origIdx);
}
void testPopupAutoConnectsThemeChange() {
auto& tm = ThemeManager::instance();
int origIdx = tm.currentIndex();
QVERIFY2(tm.themes().size() >= 2, "Need >= 2 themes");
TypeSelectorPopup popup;
// applyTheme is a public slot — verify it can be connected
connect(&tm, &ThemeManager::themeChanged,
&popup, &TypeSelectorPopup::applyTheme);
QColor bgBefore = popup.palette().color(QPalette::Window);
int otherIdx = (origIdx == 0) ? 1 : 0;
tm.setCurrent(otherIdx);
QApplication::processEvents();
// After theme change + signal, popup palette should match new theme
QCOMPARE(popup.palette().color(QPalette::Window),
tm.current().backgroundAlt);
// Restore
tm.setCurrent(origIdx);
}
// ── parseTypeSpec: primitive pointer ptrDepth ──
void testParseTypeSpecPrimitiveStar() {
TypeSpec spec = parseTypeSpec("int32_t*");
QCOMPARE(spec.baseName, QString("int32_t"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 1);
QCOMPARE(spec.arrayCount, 0);
}
void testParseTypeSpecPrimitiveDoubleStar() {
TypeSpec spec = parseTypeSpec("f64**");
QCOMPARE(spec.baseName, QString("f64"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 2);
QCOMPARE(spec.arrayCount, 0);
}
// ── Primitive pointer creation via applyTypePopupResult path ──
void testPrimitivePointerCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32) inside Alpha
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate the primitive-pointer path: Int32 → Pointer64 + elementKind=Int32 + ptrDepth=1
doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
doc->tree.nodes[idx].elementKind = NodeKind::Int32;
doc->tree.nodes[idx].ptrDepth = 1;
doc->undoStack.endMacro();
QApplication::processEvents();
// Verify: Pointer64 with elementKind=Int32, ptrDepth=1, refId=0
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
// Undo reverses the macro
doc->undoStack.undo();
QApplication::processEvents();
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Int32);
delete ctrl;
delete splitter;
delete doc;
}
void testDoublePointerCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32) inside Alpha
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate: Int32 → Pointer64 + elementKind=Double + ptrDepth=2
doc->undoStack.beginMacro(QStringLiteral("Change to double pointer"));
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
doc->tree.nodes[idx].elementKind = NodeKind::Double;
doc->tree.nodes[idx].ptrDepth = 2;
doc->undoStack.endMacro();
QApplication::processEvents();
// Verify: Pointer64 with elementKind=Double, ptrDepth=2
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── ptrDepth JSON round-trip ──
void testPtrDepthJsonRoundTrip() {
Node n;
n.kind = NodeKind::Pointer64;
n.name = "pData";
n.elementKind = NodeKind::Float;
n.ptrDepth = 2;
n.id = 42;
QJsonObject obj = n.toJson();
QCOMPARE(obj["ptrDepth"].toInt(), 2);
Node restored = Node::fromJson(obj);
QCOMPARE(restored.ptrDepth, 2);
QCOMPARE(restored.elementKind, NodeKind::Float);
QCOMPARE(restored.kind, NodeKind::Pointer64);
}
void testPtrDepthJsonDefault() {
// Nodes without ptrDepth in JSON should default to 0
Node n;
n.kind = NodeKind::Pointer64;
n.name = "pVoid";
n.id = 99;
QJsonObject obj = n.toJson();
// ptrDepth==0 is not serialized
QVERIFY(!obj.contains("ptrDepth"));
Node restored = Node::fromJson(obj);
QCOMPARE(restored.ptrDepth, 0);
}
// ── setMode always resets modifier buttons ──
void testSetModeResetsModifierInPointerTargetMode() {
TypeSelectorPopup popup;
// Set FieldType mode and select * modifier
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(1); // select *
// Now switch to PointerTarget mode — should reset to plain
popup.setMode(TypePopupMode::PointerTarget);
// Verify: modifier buttons are hidden but internally reset to plain (modId=0)
// This means primitives will be visible in applyFilter
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = "int32_t";
TypeEntry voidEntry;
voidEntry.entryKind = TypeEntry::Primitive;
voidEntry.primitiveKind = NodeKind::Pointer64;
voidEntry.displayName = "void";
popup.setTypes({prim, voidEntry});
// Both primitives should be visible (not filtered out)
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
int rowCount = listView->model()->rowCount();
// Should have section header + 2 primitives = at least 3 rows
QVERIFY2(rowCount >= 3,
qPrintable(QString("Expected >=3 rows (header+2 prims), got %1").arg(rowCount)));
}
// ── setModifier preselection ──
void testSetModifierPreselects() {
TypeSelectorPopup popup;
// Test * preselection
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(1);
auto* btnGroup = popup.findChild<QButtonGroup*>();
QVERIFY(btnGroup);
QCOMPARE(btnGroup->checkedId(), 1);
// Test ** preselection
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(2);
QCOMPARE(btnGroup->checkedId(), 2);
// Test [n] preselection with count
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(3, 8);
QCOMPARE(btnGroup->checkedId(), 3);
auto* countEdit = popup.findChild<QLineEdit*>(QStringLiteral("arrayCountEdit"));
// Array count edit may not have objectName set; find via parent
// Just verify button group is correct
}
// ── isValidPrimitivePtrTarget ──
void testIsValidPrimitivePtrTarget() {
// Hex types → NOT valid (deref shows same hex as void*)
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex8));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex16));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex64));
// Pointer types → NOT valid (use composite * for chains)
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer64));
// Function pointers → NOT valid
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr64));
// Containers → NOT valid
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Struct));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Array));
// Value types → valid
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Int32));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UInt64));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Float));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Double));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Bool));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Vec3));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UTF8));
}
// ── hex64* falls back to void* ──
void testHex64StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32)
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Build a TypeEntry for hex64
TypeEntry hexEntry;
hexEntry.entryKind = TypeEntry::Primitive;
hexEntry.primitiveKind = NodeKind::Hex64;
hexEntry.displayName = "hex64";
// Apply it with pointer modifier (fullText = "hex64*")
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
hexEntry, QStringLiteral("hex64*"));
QApplication::processEvents();
// Should be a void pointer: Pointer64, ptrDepth=0, refId=0
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testHex8StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry hexEntry;
hexEntry.entryKind = TypeEntry::Primitive;
hexEntry.primitiveKind = NodeKind::Hex8;
hexEntry.displayName = "hex8";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
hexEntry, QStringLiteral("hex8*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testPtr64StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry ptrEntry;
ptrEntry.entryKind = TypeEntry::Primitive;
ptrEntry.primitiveKind = NodeKind::Pointer64;
ptrEntry.displayName = "ptr64";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
ptrEntry, QStringLiteral("ptr64*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── Valid primitive pointers still work ──
void testInt32StarStillCreatesPrimitivePointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry intEntry;
intEntry.entryKind = TypeEntry::Primitive;
intEntry.primitiveKind = NodeKind::Int32;
intEntry.displayName = "int32_t";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
intEntry, QStringLiteral("int32_t*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testDoubleDoubleStarStillCreatesPrimitivePointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry dblEntry;
dblEntry.entryKind = TypeEntry::Primitive;
dblEntry.primitiveKind = NodeKind::Double;
dblEntry.displayName = "double";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
dblEntry, QStringLiteral("double**"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── Defense: compose/format treat invalid ptrDepth as void* ──
void testComposeShowsVoidPtrForHexPtrDepth() {
// If a node somehow has ptrDepth>0 with hex elementKind
// (e.g. from old JSON), compose should show "void*" not "hex64*"
NodeTree tree;
tree.baseAddress = 0x1000;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
tree.addNode(root);
uint64_t rootId = tree.nodes[0].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "badPtr";
ptr.parentId = rootId;
ptr.offset = 0;
ptr.ptrDepth = 1;
ptr.elementKind = NodeKind::Hex64; // invalid target
tree.addNode(ptr);
QByteArray buf(0x100, '\0');
BufferProvider prov(buf);
ComposeResult result = compose(tree, prov);
// The composed text should NOT contain "hex64*" — the invalid target
// should fall through to normal void pointer display
QVERIFY2(!result.text.contains("hex64*"),
qPrintable("Should not show 'hex64*', got: " + result.text));
}
}; };
QTEST_MAIN(TestTypeSelector) QTEST_MAIN(TestTypeSelector)

View File

@@ -0,0 +1,332 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "typeselectorpopup.h"
#include "core.h"
#include "providers/buffer_provider.h"
using namespace rcx;
static QByteArray makeBuffer() { return QByteArray(0x200, '\0'); }
// Build a tree with one root struct + a Pointer64 field
static void buildPointerTree(NodeTree& tree, const QString& rootName) {
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
root.structTypeName = rootName;
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = rootId;
ptr.offset = 0;
tree.addNode(ptr);
}
class TestTypeVisibility : public QObject {
Q_OBJECT
private slots:
// ── 1. New types created via createNewTypeRequested get a default name ──
void testCreateNewTypeGetsDefaultName() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int nodesBefore = doc->tree.nodes.size();
// Simulate what createNewTypeRequested does: create struct with default name
// (The actual handler is a lambda; we test the result via tree inspection)
{
bool wasSuppressed = ctrl->document() != nullptr; Q_UNUSED(wasSuppressed);
// Generate unique default name — same logic as the handler
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0;
n.offset = 0;
n.id = doc->tree.reserveId();
doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{n}));
}
ctrl->refresh();
QApplication::processEvents();
// Verify new struct was created with a name
QCOMPARE(doc->tree.nodes.size(), nodesBefore + 1);
bool found = false;
for (const auto& n : doc->tree.nodes) {
if (n.structTypeName == "NewClass") { found = true; break; }
}
QVERIFY2(found, "New struct should have structTypeName 'NewClass'");
delete ctrl;
delete splitter;
delete doc;
}
// ── 2. Second new type gets incremented name ──
void testCreateNewTypeIncrementsName() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
// Add a struct already named "NewClass"
{
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = "NewClass";
n.name = "instance";
n.parentId = 0;
n.offset = 0;
doc->tree.addNode(n);
}
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Generate name using same logic
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
QCOMPARE(typeName, QStringLiteral("NewClass1"));
delete ctrl;
delete splitter;
delete doc;
}
// ── 3. Cross-tab: types from other documents visible via project docs ──
void testCrossTabTypesVisible() {
// Doc A: has "Alpha" struct with a Pointer64 field
auto* docA = new RcxDocument();
buildPointerTree(docA->tree, "Alpha");
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
// Doc B: has "Beta" struct
auto* docB = new RcxDocument();
buildPointerTree(docB->tree, "Beta");
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
// Shared doc list (simulates MainWindow::m_allDocs)
QVector<RcxDocument*> allDocs;
allDocs << docA << docB;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(docA, nullptr);
ctrl->addSplitEditor(splitter);
ctrl->setProjectDocuments(&allDocs);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the Pointer64 node in docA
int ptrIdx = -1;
for (int i = 0; i < docA->tree.nodes.size(); i++) {
if (docA->tree.nodes[i].kind == NodeKind::Pointer64) {
ptrIdx = i;
break;
}
}
QVERIFY(ptrIdx >= 0);
// Apply an external type (structId=0, displayName="Beta") as pointer target
TypeEntry extEntry;
extEntry.entryKind = TypeEntry::Composite;
extEntry.structId = 0; // external sentinel
extEntry.displayName = QStringLiteral("Beta");
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
extEntry, QString());
QApplication::processEvents();
// "Beta" should now exist in docA as a local struct (imported)
bool found = false;
uint64_t betaLocalId = 0;
for (const auto& n : docA->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& n.structTypeName == "Beta") {
found = true;
betaLocalId = n.id;
break;
}
}
QVERIFY2(found, "Beta struct should be imported into docA");
// The pointer's refId should point at the local Beta
int ptrIdx2 = -1;
for (int i = 0; i < docA->tree.nodes.size(); i++) {
if (docA->tree.nodes[i].kind == NodeKind::Pointer64
&& docA->tree.nodes[i].name == "ptr") {
ptrIdx2 = i;
break;
}
}
QVERIFY(ptrIdx2 >= 0);
QCOMPARE(docA->tree.nodes[ptrIdx2].refId, betaLocalId);
delete ctrl;
delete splitter;
delete docA;
delete docB;
}
// ── 4. findOrCreateStructByName reuses existing local struct ──
void testFindOrCreateReusesExisting() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
// Add "Target" struct manually
Node target;
target.kind = NodeKind::Struct;
target.structTypeName = "Target";
target.name = "instance";
target.parentId = 0;
target.offset = 0;
int ti = doc->tree.addNode(target);
uint64_t targetId = doc->tree.nodes[ti].id;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int nodesBefore = doc->tree.nodes.size();
// Apply external entry with name "Target" — should reuse existing
int ptrIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].kind == NodeKind::Pointer64) {
ptrIdx = i;
break;
}
}
QVERIFY(ptrIdx >= 0);
TypeEntry extEntry;
extEntry.entryKind = TypeEntry::Composite;
extEntry.structId = 0;
extEntry.displayName = QStringLiteral("Target");
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
extEntry, QString());
QApplication::processEvents();
// Should NOT have created a new struct — reused existing one
QCOMPARE(doc->tree.nodes.size(), nodesBefore);
// Pointer should reference the existing Target
int ptrIdx2 = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].kind == NodeKind::Pointer64
&& doc->tree.nodes[i].name == "ptr") {
ptrIdx2 = i;
break;
}
}
QVERIFY(ptrIdx2 >= 0);
QCOMPARE(doc->tree.nodes[ptrIdx2].refId, targetId);
delete ctrl;
delete splitter;
delete doc;
}
// ── 5. External types skip duplicates already in local doc ──
void testExternalTypesSkipLocalDuplicates() {
// Both docs have "Shared" type — should not appear twice
auto* docA = new RcxDocument();
buildPointerTree(docA->tree, "Shared");
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* docB = new RcxDocument();
buildPointerTree(docB->tree, "Shared");
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
QVector<RcxDocument*> allDocs;
allDocs << docA << docB;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(docA, nullptr);
ctrl->addSplitEditor(splitter);
ctrl->setProjectDocuments(&allDocs);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Count how many "Shared" entries exist in local doc's root structs
int sharedCount = 0;
for (const auto& n : docA->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& n.structTypeName == "Shared")
sharedCount++;
}
QCOMPARE(sharedCount, 1); // only the local one
delete ctrl;
delete splitter;
delete docA;
delete docB;
}
};
QTEST_MAIN(TestTypeVisibility)
#include "test_type_visibility.moc"

View File

@@ -16,7 +16,7 @@ using namespace rcx;
// ── Fixture: small tree with diverse field types ── // ── Fixture: small tree with diverse field types ──
static void buildValidationTree(NodeTree& tree) { static void buildValidationTree(NodeTree& tree) {
tree.baseAddress = 0x1000; tree.baseAddress = 0;
Node root; Node root;
root.kind = NodeKind::Struct; root.kind = NodeKind::Struct;

View File

@@ -260,17 +260,6 @@ private slots:
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16); qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
} }
void provider_setBase()
{
WinDbgMemoryProvider prov(m_connString);
QVERIFY(prov.isValid());
uint64_t orig = prov.base();
prov.setBase(0x1000);
QCOMPARE(prov.base(), (uint64_t)0x1000);
prov.setBase(orig);
QCOMPARE(prov.base(), orig);
}
// ── Read: MZ header on main thread ── // ── Read: MZ header on main thread ──
void provider_read_mz_mainThread() void provider_read_mz_mainThread()