Compare commits

...

17 Commits

Author SHA1 Message Date
IChooseYou
b089e20d36 ci: retrigger build 2026-02-20 17:01:57 -07:00
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
with:
version: '6.8.1'
arch: 'win64_msvc2022_64'
arch: 'win64_mingw'
cache: true
aqtversion: '==3.1.21'
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- 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
run: cmake --build build
shell: bash
run: |
export PATH="/c/mingw64/bin:$PATH"
cmake --build build
- 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
uses: actions/upload-artifact@v4
@@ -97,7 +103,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linux:
needs: windows
runs-on: ubuntu-22.04
steps:
@@ -118,15 +123,13 @@ jobs:
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
- 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
run: cmake --build build
- name: Test
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_controller"
env:
QT_QPA_PLATFORM: offscreen
run: ctest --test-dir build --output-on-failure
- name: Create AppImage
run: |
@@ -188,4 +191,3 @@ jobs:
files: Reclass-linux64-qt6.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -256,6 +256,20 @@ if(BUILD_TESTING)
endif()
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
src/editor.cpp src/compose.cpp src/format.cpp
src/providerregistry.cpp
@@ -302,6 +316,19 @@ if(BUILD_TESTING)
endif()
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
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)
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
# that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt)

View File

@@ -1,18 +1,30 @@
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`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge.exe",
"args": []
}
![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
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge",
"args": []
}
}
```
}
```
## Build
1. Prerequisites

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;
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)
memset((char*)buf + bytesRead, 0, len - bytesRead);
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;
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 false;
}
@@ -156,15 +156,13 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention)
struct iovec local;
local.iov_base = buf;
local.iov_len = static_cast<size_t>(len);
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);
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;
// 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);
}
@@ -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;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first
struct iovec local;
local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len);
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);
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;
// 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);
}

View File

@@ -27,11 +27,16 @@ public:
bool isLive() const override { return true; }
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
uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); }
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)
return false;
uint64_t absAddr = m_base + addr;
return m_fns.ReadRemoteMemory(m_handle,
reinterpret_cast<RC_Pointer>(absAddr),
reinterpret_cast<RC_Pointer>(addr),
static_cast<RC_Pointer>(buf),
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)
return false;
uint64_t absAddr = m_base + addr;
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)),
0, len);
}

View File

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

View File

@@ -304,7 +304,7 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
bool result = false;
dispatchToOwner([&]() {
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)
memset((char*)buf + bytesRead, 0, len - bytesRead);
result = bytesRead > 0;
@@ -324,7 +324,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
bool result = false;
dispatchToOwner([&]() {
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);
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
});
@@ -364,7 +364,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
char nameBuf[512] = {};
ULONG nameSize = 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);
if (SUCCEEDED(hr) && nameSize > 0) {
result = QString::fromUtf8(nameBuf);

View File

@@ -62,7 +62,6 @@ public:
bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
private:
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;
}
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) {
int64_t total = 0;
QSet<uint64_t> visited;
@@ -125,8 +119,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString ptrTypeOverride;
QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
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);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
}
}
for (int sub = 0; sub < numLines; sub++) {
@@ -140,8 +143,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.isContinuation = isCont;
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
@@ -187,8 +190,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
@@ -206,8 +209,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::ArrayElementSeparator;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldLevel = computeFoldLevel(depth, false);
@@ -236,8 +239,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.isRootHeader = false;
@@ -300,8 +303,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind;
lm.isArrayElement = true;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + elemAddr;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
lm.foldLevel = computeFoldLevel(childDepth, false);
@@ -353,9 +356,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.depth = childDepth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(
tree.baseAddress + absAddr + child.offset, false,
absAddr + child.offset, false,
state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
lm.offsetAddr = absAddr + child.offset;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = child.kind;
lm.foldHead = true;
@@ -399,8 +402,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap);
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + sz;
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
@@ -445,8 +448,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldHead = true;
@@ -472,26 +475,21 @@ void composeNode(ComposeState& state, const NodeTree& tree,
// Treat sentinel values as invalid pointers
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
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
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
// Pointer target address is used directly (absolute)
uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
// For invalid/unreadable pointers: use NullProvider (shows zeros)
// and reset margin offsets (unsigned wrap cancels baseAddress)
static NullProvider s_nullProv;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress;
pBase = 0;
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = tree.baseAddress + pBase;
state.currentPtrBase = pBase;
if (hasMaterialized) {
// 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++)
state.childMap[tree.nodes[i].parentId].append(i);
// Precompute absolute offsets
// Precompute absolute offsets (baseAddress + structure-relative offset)
state.absOffsets.resize(tree.nodes.size());
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
{
uint64_t maxAddr = tree.baseAddress;
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 (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;

File diff suppressed because it is too large Load Diff

View File

@@ -92,14 +92,20 @@ public:
void removeNode(int nodeIdx);
void toggleCollapse(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 convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId);
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void deleteRootStruct(uint64_t structId);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
uint64_t findOrCreateStructByName(const QString& typeName);
// Selection
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
@@ -122,6 +128,15 @@ public:
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
int activeSourceIndex() const { return m_activeSourceIdx; }
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
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
@@ -154,17 +169,19 @@ private:
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = false;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;
QVector<RcxDocument*>* m_projectDocs = nullptr;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
void switchToSavedSource(int idx);
void pushSavedSourcesToEditors();
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);
// ── Auto-refresh methods ──

View File

@@ -142,6 +142,15 @@ inline constexpr bool isMatrixKind(NodeKind k) {
inline constexpr bool isFuncPtr(NodeKind k) {
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) {
QStringList out;
@@ -184,7 +193,8 @@ struct Node {
int strLen = 64;
bool collapsed = false;
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)
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
@@ -217,6 +227,8 @@ struct Node {
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -233,6 +245,7 @@ struct Node {
n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
return n;
}

View File

@@ -20,6 +20,7 @@
#include <QLabel>
#include <QToolButton>
#include <QScreen>
#include <QScrollBar>
#include <functional>
#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_HEX_DIM = 9;
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()->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).
connect(m_sci, &QsciScintilla::marginClicked,
@@ -372,8 +488,10 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (id == 1 && (m_editState.target == EditTarget::Type
|| m_editState.target == EditTarget::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget)) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
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')));
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
(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
@@ -1012,8 +1134,13 @@ void RcxEditor::restoreViewState(const ViewState& vs) {
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, (unsigned long)pos);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
(unsigned long)vs.scrollLine);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET,
(unsigned long)vs.xOffset);
// Clamp xOffset so it doesn't exceed the current content width.
// 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 {
@@ -1399,7 +1526,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
switch (t) {
case EditTarget::Type: s = typeSpan(*lm, typeW); 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::ArrayIndex:
case EditTarget::ArrayCount:
@@ -1670,15 +1798,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Single-click on editable token of already-selected node → edit
int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
// Type/ArrayElementType/PointerTarget open a dismissible popup
// (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());
if (alreadySelected && plain) {
m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine, tCol);
}
@@ -2012,9 +2132,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_hoveredNodeId = 0;
m_hoveredLine = -1;
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)
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)
clearIndicatorLine(IND_EDITABLE, m_hintLine);
m_hintLine = -1;
@@ -2250,8 +2372,12 @@ void RcxEditor::commitInlineEdit() {
if (m_editState.target == EditTarget::Type && editedText.isEmpty())
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();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText);
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText, addr);
}
// ── Cancel inline edit ──
@@ -2337,6 +2463,9 @@ void RcxEditor::showSourcePicker() {
act->setChecked(m_savedSourceDisplay[i].active);
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);
@@ -2348,11 +2477,15 @@ void RcxEditor::showSourcePicker() {
QAction* sel = menu.exec(pos);
if (sel) {
const LineMeta* lm = metaForLine(m_editState.line);
uint64_t addr = lm ? lm->offsetAddr : 0;
auto info = endInlineEdit();
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());
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr);
} else {
cancelInlineEdit();
}
@@ -2580,9 +2713,11 @@ void RcxEditor::applyHoverCursor() {
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
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())
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
return;
}
@@ -2593,6 +2728,8 @@ void RcxEditor::applyHoverCursor() {
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
if (m_disasmPopup && !m_applyingDocument)
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup && !m_applyingDocument)
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
m_sci->viewport()->setCursor(Qt::ArrowCursor);
return;
}
@@ -2755,11 +2892,8 @@ void RcxEditor::applyHoverCursor() {
if (!isVoidPtr || node.refId == 0) {
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|| lm.nodeKind == NodeKind::Pointer64);
// Use composed address (correct for pointer-expanded nodes)
// not node.offset (which is just offset within struct definition).
uint64_t provAddr = lm.offsetAddr >= m_disasmTree->baseAddress
? lm.offsetAddr - m_disasmTree->baseAddress
: static_cast<uint64_t>(node.offset);
// Use composed address (absolute, correct for pointer-expanded nodes)
uint64_t provAddr = lm.offsetAddr;
uint64_t ptrVal = is64
? m_disasmProvider->readU64(provAddr)
: (uint64_t)m_disasmProvider->readU32(provAddr);
@@ -2768,13 +2902,11 @@ void RcxEditor::applyHoverCursor() {
// Read code bytes from the function target address.
// Use the real provider (not snapshot) because function
// code lives at arbitrary process addresses that aren't
// in the snapshot page table. The provider reads from
// m_base + addr via ReadProcessMemory, so we convert
// the absolute ptrVal to provider-relative.
// in the snapshot page table.
const Provider* codeProv = m_disasmRealProv
? m_disasmRealProv : m_disasmProvider;
constexpr int kMaxRead = 128;
uint64_t codeAddr = ptrVal - m_disasmTree->baseAddress;
uint64_t codeAddr = ptrVal;
QByteArray bytes(kMaxRead, Qt::Uninitialized);
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
if (readOk) {
@@ -2837,6 +2969,70 @@ void RcxEditor::applyHoverCursor() {
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
Qt::CursorShape desired = Qt::ArrowCursor;

View File

@@ -27,6 +27,7 @@ public:
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId);
@@ -68,7 +69,8 @@ signals:
void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled();
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
@@ -138,6 +140,7 @@ private:
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (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_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;

View File

@@ -267,6 +267,30 @@ static QString readValueImpl(const Node& node, const Provider& prov,
}
case NodeKind::Pointer64: {
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);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);

View File

@@ -1,4 +1,5 @@
#include "mainwindow.h"
#include "providerregistry.h"
#include "generator.h"
#include "import_reclass_xml.h"
#include "import_source.h"
@@ -44,6 +45,8 @@
#include <Qsci/qscilexercpp.h>
#include <QProxyStyle>
#include <QDesktopServices>
#include <QWindow>
#include <QMouseEvent>
#include "themes/thememanager.h"
#include "themes/themeeditor.h"
#include "optionsdialog.h"
@@ -205,6 +208,9 @@ public:
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
if (elem == PE_FrameMenu)
return;
// Kill the status bar item frame and panel border
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
return;
QProxyStyle::drawPrimitive(elem, opt, p, w);
}
void drawControl(ControlElement element, const QStyleOption* opt,
@@ -321,6 +327,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
overlay->show();
m_mdiArea = new QMdiArea(this);
m_mdiArea->setFrameShape(QFrame::NoFrame);
m_mdiArea->setViewMode(QMdiArea::TabbedView);
m_mdiArea->setTabsClosable(true);
m_mdiArea->setTabsMovable(true);
@@ -341,6 +348,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createMenus();
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)
{
QSettings s("Reclass", "Reclass");
@@ -376,6 +390,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
for (int i = 0; i < tab->panes.size(); ++i) {
if (tab->panes[i].tabWidget && tab->panes[i].tabWidget->isAncestorOf(now)) {
tab->activePaneIdx = i;
syncViewButtons(tab->panes[i].viewMode);
return;
}
}
@@ -407,6 +422,9 @@ void MainWindow::createMenus() {
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
file->addSeparator();
m_sourceMenu = file->addMenu("Current Tab So&urce");
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
file->addSeparator();
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
file->addSeparator();
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
@@ -492,11 +510,188 @@ void MainWindow::createMenus() {
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() {
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);
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();
QPalette sbPal = statusBar()->palette();
@@ -504,22 +699,31 @@ void MainWindow::createStatusBar() {
sbPal.setColor(QPalette::WindowText, t.textDim);
statusBar()->setPalette(sbPal);
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() {
auto* tabBar = m_mdiArea->findChild<QTabBar*>();
@@ -557,7 +761,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
pane.tabWidget = new QTabWidget;
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)
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
@@ -574,18 +779,20 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
// Add to splitter
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;
connect(tw, &QTabWidget::currentChanged, this, [this, tw](int index) {
// Find which pane this QTabWidget belongs to
SplitPane* p = findPaneByTabWidget(tw);
if (!p) return;
if (index == 1) p->viewMode = VM_Rendered;
else p->viewMode = VM_Reclass;
p->viewMode = (index == 1) ? VM_Rendered : 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) {
// Find the TabState that owns this pane and update rendered view
for (auto& tab : m_tabs) {
for (auto& pane : tab.panes) {
if (&pane == p) {
@@ -642,6 +849,7 @@ static QString rootName(const NodeTree& tree, uint64_t viewRootId = 0) {
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
auto* splitter = new QSplitter(Qt::Horizontal);
splitter->setHandleWidth(1);
auto* ctrl = new RcxController(doc, splitter);
auto* sub = m_mdiArea->addSubWindow(splitter);
@@ -657,12 +865,17 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
// Create the initial split pane
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]() {
auto it = m_tabs.find(sub);
if (it != m_tabs.end()) {
it->doc->deleteLater();
m_tabs.erase(it);
}
rebuildAllDocs();
rebuildWorkspaceModel();
});
@@ -1034,6 +1247,9 @@ void MainWindow::toggleMcp() {
void MainWindow::applyTheme(const Theme& theme) {
applyGlobalTheme(theme);
// Kill the 1px separator line between central widget and status bar
setStyleSheet("QMainWindow::separator { height: 0px; width: 0px; }");
// Custom title bar
m_titleBar->applyTheme(theme);
@@ -1060,6 +1276,24 @@ void MainWindow::applyTheme(const Theme& theme) {
sbPal.setColor(QPalette::WindowText, theme.textDim);
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
if (m_workspaceTree) {
@@ -1068,10 +1302,44 @@ void MainWindow::applyTheme(const Theme& theme) {
m_workspaceTree->setPalette(tp);
}
// Split pane tab widgets
for (auto& state : m_tabs) {
for (auto& pane : state.panes) {
if (pane.tabWidget) applyTabWidgetStyle(pane.tabWidget);
// Dock titlebar: restyle label + close button
if (m_dockTitleLabel)
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
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
if (m_workspaceTree)
m_workspaceTree->setFont(f);
// Sync dock titlebar font
if (m_dockTitleLabel)
m_dockTitleLabel->setFont(f);
// Sync status bar font
statusBar()->setFont(f);
m_btnReclass->setFont(f);
m_btnRendered->setFont(f);
}
RcxController* MainWindow::activeController() const {
@@ -1268,7 +1541,13 @@ void MainWindow::setViewMode(ViewMode mode) {
pane->viewMode = mode;
int idx = (mode == VM_Rendered) ? 1 : 0;
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 ──
@@ -1635,6 +1914,42 @@ void MainWindow::createWorkspaceDock() {
m_workspaceDock = new QDockWidget("Project Tree", this);
m_workspaceDock->setObjectName("WorkspaceDock");
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_workspaceModel = new QStandardItemModel(this);
@@ -1689,7 +2004,53 @@ void MainWindow::createWorkspaceDock() {
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
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();
} else if (chosen && chosen == actConvert) {
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() {
QVector<rcx::TabInfo> tabs;
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
@@ -1744,6 +2111,41 @@ void MainWindow::rebuildWorkspaceModel() {
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() {
QDialog dialog(this);
dialog.setWindowTitle("Plugins");
@@ -1860,6 +2262,11 @@ void MainWindow::resizeEvent(QResizeEvent* event) {
m_borderOverlay->setGeometry(rect());
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) {

View File

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

View File

@@ -287,7 +287,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"name", "hex.read"},
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
"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{
{"type", "object"},
{"properties", QJsonObject{
@@ -825,8 +826,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int length = qMin(args.value("length").toInt(64), 4096);
if (args.value("baseRelative").toBool())
offset -= (int64_t)tab->doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)tab->doc->tree.baseAddress;
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
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());
QString hexStr = args.value("hexBytes").toString().remove(' ');
if (args.value("baseRelative").toBool())
offset -= (int64_t)doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)doc->tree.baseAddress;
if (hexStr.size() % 2 != 0)
return makeTextResult("Hex string must have even length", true);

View File

@@ -33,10 +33,10 @@ public:
// Examples: "File", "Process", "Socket"
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.
virtual uint64_t base() const { return 0; }
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
// Resolve an absolute address to a symbol name.
// Returns empty string if no symbol is known.

View File

@@ -18,7 +18,11 @@ ThemeManager::ThemeManager() {
loadUserThemes();
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();
auto all = themes();
for (int i = 0; i < all.size(); i++) {

View File

@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
if (s.endsWith('*')) {
spec.isPointer = true;
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();
return spec;
}
@@ -97,6 +98,12 @@ public:
int h = option.rect.height();
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
if (isSection) {
painter->setPen(t.textDim);
@@ -133,18 +140,18 @@ public:
if (isCurrent) {
painter->setPen(t.text);
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)));
}
}
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()
&& (*m_filtered)[row].entryKind == TypeEntry::Composite);
if (hasIcon) {
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
QPixmap pm = structIcon.pixmap(16, 16);
QPixmap pm = structIcon.pixmap(iconSz, iconSz);
if (isDisabled) {
// Paint dimmed
QPixmap dimmed(pm.size());
@@ -153,12 +160,12 @@ public:
p.setOpacity(0.35);
p.drawPixmap(0, 0, pm);
p.end();
painter->drawPixmap(x, y + (h - 16) / 2, dimmed);
painter->drawPixmap(x, y + (h - iconSz) / 2, dimmed);
} 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
QColor textColor;
@@ -273,14 +280,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
// Separator
{
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
m_separator = new QFrame;
m_separator->setFrameShape(QFrame::HLine);
m_separator->setFrameShadow(QFrame::Plain);
QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, theme.border);
sep->setPalette(sepPal);
sep->setFixedHeight(1);
layout->addWidget(sep);
m_separator->setPalette(sepPal);
m_separator->setFixedHeight(1);
layout->addWidget(m_separator);
}
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
@@ -341,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
applyFilter(m_filterEdit->text());
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); });
@@ -450,28 +456,92 @@ void TypeSelectorPopup::setFont(const QFont& 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) {
m_titleLabel->setText(title);
}
void TypeSelectorPopup::setMode(TypePopupMode mode) {
m_mode = mode;
// Show modifier toggles for modes where type modifiers make sense
bool showMods = (mode == TypePopupMode::FieldType
|| mode == TypePopupMode::ArrayElement);
m_modRow->setVisible(showMods);
// Reset to plain when showing
if (showMods) {
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
// Always reset to plain — prevents stale state from leaking across modes
// (PointerTarget hides buttons but applyFilter still reads their state)
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
void TypeSelectorPopup::setCurrentNodeSize(int 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) {
m_allTypes = types;
if (current) {
@@ -481,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
m_currentEntry = TypeEntry{};
m_hasCurrent = false;
}
// Reset modifier toggles
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
// Don't reset modifier buttons here — setMode() already resets to plain,
// and setModifier() may have preselected a button between setMode/setTypes.
m_previewLabel->hide();
m_filterEdit->clear();
@@ -498,7 +566,9 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
QString text = t.classKeyword.isEmpty()
? 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;
}
int popupW = qBound(280, maxTextW + 24, 500);
@@ -568,27 +638,26 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
QString filterBase = text.trimmed();
// Hide primitives when a pointer modifier (* or **) is active
int modId = m_modGroup->checkedId();
bool hideprimitives = (modId == 1 || modId == 2);
// Separate primitives and composites
// Separate primitives and composites (all types shown regardless of modifier)
QVector<TypeEntry> primitives, composites;
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()
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
if (!matchesFilter) continue;
if (t.entryKind == TypeEntry::Primitive) {
if (!hideprimitives)
primitives.append(t);
} else if (t.entryKind == TypeEntry::Composite)
if (t.entryKind == TypeEntry::Primitive)
primitives.append(t);
else if (t.entryKind == TypeEntry::Composite)
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()) {
QVector<TypeEntry> sameSize, other;
for (const auto& p : primitives) {
@@ -597,7 +666,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
else
other.append(p);
}
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
std::sort(other.begin(), other.end(), alphabetical);
primitives = sameSize + other;
} else {
std::sort(primitives.begin(), primitives.end(), alphabetical);
}
// Helper lambdas for appending sections

View File

@@ -16,6 +16,8 @@ class QWidget;
namespace rcx {
struct Theme;
// ── Popup mode ──
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
@@ -38,6 +40,7 @@ struct TypeEntry {
struct TypeSpec {
QString baseName;
bool isPointer = false;
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
int arrayCount = 0; // 0 = not array
};
@@ -53,7 +56,9 @@ public:
void setFont(const QFont& font);
void setTitle(const QString& title);
void setMode(TypePopupMode mode);
void applyTheme(const Theme& theme);
void setCurrentNodeSize(int bytes);
void setModifier(int modId, int arrayCount = 0);
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
void popup(const QPoint& globalPos);
@@ -77,6 +82,7 @@ private:
QLabel* m_previewLabel = nullptr;
QListView* m_listView = nullptr;
QStringListModel* m_model = nullptr;
QFrame* m_separator = nullptr;
// Modifier toggles
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() {
// Expanded primitive array should synthesize element lines dynamically
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -1934,7 +1934,7 @@ private slots:
void testTextIsNonEmpty() {
// Verify composed text is actually generated (not empty)
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -8,7 +8,7 @@
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -394,6 +394,65 @@ private slots:
QApplication::processEvents();
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)

View File

@@ -22,7 +22,6 @@ public:
}
int size() const override { return m_data.size(); }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
bool isLive() const override { return true; }
QString name() const override { return QStringLiteral("test"); }
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.
// Keeps tests fast and deterministic (no giant PEB tree).
static void buildSmallTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -405,7 +404,8 @@ private slots:
// ── Test: source switch preserves existing base address ──
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);
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
@@ -414,16 +414,14 @@ private slots:
QCOMPARE(newBase, (uint64_t)0x400000);
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)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// baseAddress must stay at the original value
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// provider base must be synced to match
QCOMPARE(m_doc->provider->base(), (uint64_t)0x1000);
// provider base is unchanged (no setBase sync) — provider reports its own initial base
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
}
// ── Test: source switch on fresh doc uses provider default ──
@@ -437,12 +435,9 @@ private slots:
m_doc->provider = prov;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
else
m_doc->provider->setBase(m_doc->tree.baseAddress);
// Fresh doc should adopt the provider's default base
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
QCOMPARE(m_doc->provider->base(), (uint64_t)0x7FFE0000);
}
// ── Test: toggleCollapse + undo ──

View File

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

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 ──
static void buildValidationTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -260,17 +260,6 @@ private slots:
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 ──
void provider_read_mz_mainThread()