Compare commits

...

12 Commits

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:29:18 -07:00
23 changed files with 2563 additions and 447 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

@@ -119,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++) {

View File

@@ -223,14 +223,24 @@ void RcxController::connectEditor(RcxEditor* editor) {
TypePopupMode mode = TypePopupMode::FieldType;
if (target == EditTarget::ArrayElementType)
mode = TypePopupMode::ArrayElement;
else if (target == EditTarget::PointerTarget)
mode = TypePopupMode::PointerTarget;
else if (target == EditTarget::PointerTarget) {
// Primitive pointers (ptrDepth>0) should open FieldType with
// the base type selected and *//** preselected — not PointerTarget.
bool isPrimPtr = false;
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
const auto& n = m_doc->tree.nodes[nodeIdx];
isPrimPtr = n.ptrDepth > 0 && n.refId == 0;
}
mode = isPrimPtr ? TypePopupMode::FieldType
: TypePopupMode::PointerTarget;
}
showTypePopup(editor, mode, nodeIdx, globalPos);
});
// Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) {
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
uint64_t resolvedAddr) {
// CommandRow BaseAddress/Source/RootClass edit has nodeIdx=-1
if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source
&& target != EditTarget::RootClassType && target != EditTarget::RootClassName) { refresh(); return; }
@@ -241,7 +251,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
const Node& node = m_doc->tree.nodes[nodeIdx];
// ASCII edit on Hex nodes
if (isHexPreview(node.kind)) {
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr);
} else {
renameNode(nodeIdx, text);
}
@@ -311,7 +321,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
break;
}
case EditTarget::Value:
setNodeValue(nodeIdx, subLine, text);
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr);
break;
case EditTarget::BaseAddress: {
QString s = text.trimmed();
@@ -368,123 +378,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
}
break;
}
case EditTarget::Source: {
if (text.startsWith(QStringLiteral("#saved:"))) {
int idx = text.mid(7).toInt();
switchToSavedSource(idx);
} else if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) {
// Save current source's base address before switching
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
m_doc->loadData(path);
// Check if this file is already saved
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == QStringLiteral("File")
&& m_savedSources[i].filePath == path) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_doc->tree.baseAddress = m_savedSources[existingIdx].baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = QFileInfo(path).fileName();
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
}
}
else
{
// Look up provider in registry
const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", ""));
if (providerInfo) {
QString target;
bool selected = false;
// Execute provider's target selection
if (providerInfo->isBuiltin) {
// Built-in provider with factory function
if (providerInfo->factory) {
selected = providerInfo->factory(qobject_cast<QWidget*>(parent()), &target);
}
} else {
// Plugin-based provider
if (providerInfo->plugin) {
selected = providerInfo->plugin->selectTarget(qobject_cast<QWidget*>(parent()), &target);
}
}
if (selected && !target.isEmpty()) {
// Create provider from target
std::unique_ptr<Provider> provider;
QString errorMsg;
if (providerInfo->plugin)
{
provider = providerInfo->plugin->createProvider(target, &errorMsg);
}
// Apply provider or show error
if (provider) {
// Save current source's base address before switching
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
uint64_t newBase = provider->base();
QString displayName = provider->name();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
resetSnapshot();
emit m_doc->documentChanged();
// Save as a source for quick-switch
QString identifier = providerInfo->identifier;
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == identifier
&& m_savedSources[i].providerTarget == target) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_savedSources[existingIdx].baseAddress = m_doc->tree.baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = identifier;
entry.displayName = displayName;
entry.providerTarget = target;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
} else if (!errorMsg.isEmpty()) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
}
}
}
}
case EditTarget::Source:
selectSource(text);
break;
}
case EditTarget::ArrayElementType: {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) break;
const Node& node = m_doc->tree.nodes[nodeIdx];
@@ -890,6 +786,48 @@ void RcxController::removeNode(int nodeIdx) {
cmd::Remove{nodeId, subtree, adjs}));
}
void RcxController::deleteRootStruct(uint64_t structId) {
int ni = m_doc->tree.indexOfId(structId);
if (ni < 0) return;
const Node& node = m_doc->tree.nodes[ni];
if (node.parentId != 0 || node.kind != NodeKind::Struct) return;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Delete root struct"));
// Clear all refId references pointing to this struct
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
auto& n = m_doc->tree.nodes[i];
if (n.refId == structId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{n.id, n.refId, (uint64_t)0}));
}
}
// Remove the struct + subtree (re-lookup since commands may shift indices)
ni = m_doc->tree.indexOfId(structId);
if (ni >= 0)
removeNode(ni);
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
// Switch view if we just deleted the viewed root
if (m_viewRootId == structId) {
uint64_t nextRoot = 0;
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
nextRoot = n.id;
break;
}
}
setViewRootId(nextRoot);
}
if (!m_suppressRefresh) refresh();
}
void RcxController::toggleCollapse(int nodeIdx) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
@@ -1095,14 +1033,22 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
}
void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii) {
bool isAscii, uint64_t resolvedAddr) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
if (!m_doc->provider->isWritable()) return;
const Node& node = m_doc->tree.nodes[nodeIdx];
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
if (signedAddr < 0) return; // malformed tree: negative offset
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
// Use the compose-resolved address when available (correct for pointer children).
// Fall back to tree.baseAddress + computeOffset for callers that don't supply it.
uint64_t addr;
if (resolvedAddr != 0) {
addr = resolvedAddr;
} else {
int64_t signedAddr = m_doc->tree.computeOffset(nodeIdx);
if (signedAddr < 0) return; // malformed tree: negative offset
addr = m_doc->tree.baseAddress + static_cast<uint64_t>(signedAddr);
}
// For vector components, redirect to float parsing at sub-offset
NodeKind editKind = node.kind;
@@ -1918,6 +1864,8 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
QVector<TypeEntry> entries;
TypeEntry currentEntry;
bool hasCurrent = false;
int preModId = 0; // modifier to preselect: 0=plain, 1=*, 2=**, 3=[n]
int preArrayCount = 0; // array count when preModId==3
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
for (const auto& m : kKindMeta) {
@@ -1957,10 +1905,43 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
});
break;
case TypePopupMode::FieldType:
case TypePopupMode::FieldType: {
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/false);
if (node) {
// Mark current primitive
bool isPtr = node
&& (node->kind == NodeKind::Pointer32 || node->kind == NodeKind::Pointer64);
bool isTypedPtr = isPtr && node->refId != 0;
bool isPrimPtr = isPtr && node->ptrDepth > 0 && node->refId == 0;
bool isArray = node && node->kind == NodeKind::Array;
if (isPrimPtr) {
// Primitive pointer (e.g. int32* or f64**) — current = element kind, modifier = *//**
preModId = (node->ptrDepth >= 2) ? 2 : 1;
for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
currentEntry = e;
hasCurrent = true;
break;
}
}
} else if (isTypedPtr) {
// Typed pointer (e.g. Ball*) — current = composite target, modifier = *
preModId = 1;
} else if (isArray) {
// Array — modifier = [n]
preModId = 3;
preArrayCount = node->arrayLen;
if (node->elementKind != NodeKind::Struct) {
// Primitive array — mark element kind as current
for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->elementKind) {
currentEntry = e;
hasCurrent = true;
break;
}
}
}
} else if (node) {
// Plain primitive — mark current
for (auto& e : entries) {
if (e.entryKind == TypeEntry::Primitive && e.primitiveKind == node->kind) {
currentEntry = e;
@@ -1969,8 +1950,14 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}
}
}
addComposites([](const Node&, const TypeEntry&) { return false; });
// For isTypedPtr or struct-array: current is a Composite, set by addComposites below
addComposites([&](const Node& n, const TypeEntry& e) {
if (isTypedPtr && n.refId == e.structId) return true;
if (isArray && n.elementKind == NodeKind::Struct && n.refId == e.structId) return true;
return false;
});
break;
}
case TypePopupMode::ArrayElement:
addPrimitives(/*enabled=*/true, /*excludeStructArrayPad=*/true);
@@ -2007,6 +1994,29 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
}
}
// ── Add types from other open documents (not for Root mode) ──
if (mode != TypePopupMode::Root && m_projectDocs) {
QSet<QString> localNames;
for (const auto& e : entries)
if (e.entryKind == TypeEntry::Composite)
localNames.insert(e.displayName);
for (auto* doc : *m_projectDocs) {
if (doc == m_doc) continue;
for (const auto& n : doc->tree.nodes) {
if (n.parentId != 0 || n.kind != NodeKind::Struct) continue;
QString name = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
if (name.isEmpty() || localNames.contains(name)) continue;
localNames.insert(name);
TypeEntry e;
e.entryKind = TypeEntry::Composite;
e.structId = 0; // sentinel: not in local tree yet
e.displayName = name;
e.classKeyword = n.resolvedClassKeyword();
entries.append(e);
}
}
}
// ── Font with zoom ──
QSettings settings("Reclass", "Reclass");
QString fontName = settings.value("font", "JetBrains Mono").toString();
@@ -2034,6 +2044,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
popup->setFont(font);
popup->setMode(mode);
// Preselect modifier button to reflect current node state (after setMode resets to plain)
if (preModId > 0)
popup->setModifier(preModId, preArrayCount);
// Pass current node size for same-size sorting
int nodeSize = 0;
if (node) {
@@ -2059,9 +2073,22 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
// Generate unique default type name
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : m_doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
Node n;
n.kind = NodeKind::Struct;
n.name = QString();
n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0;
n.offset = 0;
n.id = m_doc->tree.reserveId();
@@ -2087,9 +2114,16 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
const TypeEntry& entry, const QString& fullText) {
// Resolve external types: structId==0 means from another document, import first
TypeEntry resolved = entry;
if (resolved.entryKind == TypeEntry::Composite && resolved.structId == 0
&& !resolved.displayName.isEmpty()) {
resolved.structId = findOrCreateStructByName(resolved.displayName);
}
if (mode == TypePopupMode::Root) {
if (entry.entryKind == TypeEntry::Composite)
setViewRootId(entry.structId);
if (resolved.entryKind == TypeEntry::Composite)
setViewRootId(resolved.structId);
return;
}
@@ -2108,7 +2142,7 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
TypeSpec spec = parseTypeSpec(fullText);
if (mode == TypePopupMode::FieldType) {
if (entry.entryKind == TypeEntry::Primitive) {
if (resolved.entryKind == TypeEntry::Primitive) {
if (spec.arrayCount > 0) {
// Primitive array: e.g. "int32_t[10]"
bool wasSuppressed = m_suppressRefresh;
@@ -2119,19 +2153,57 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != entry.primitiveKind || n.arrayLen != spec.arrayCount)
if (n.elementKind != resolved.primitiveKind || n.arrayLen != spec.arrayCount)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, entry.primitiveKind,
cmd::ChangeArrayMeta{nodeId, n.elementKind, resolved.primitiveKind,
n.arrayLen, spec.arrayCount}));
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
} else if (spec.isPointer) {
if (!isValidPrimitivePtrTarget(resolved.primitiveKind)) {
// Hex, pointer, fnptr types with * → plain void pointer
if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
n.ptrDepth = 0;
if (n.refId != 0)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, 0}));
}
} else {
// Primitive pointer: e.g. "int32*" or "f64**" → Pointer64 + elementKind + ptrDepth
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != resolved.primitiveKind || n.ptrDepth != spec.ptrDepth) {
NodeKind oldEK = n.elementKind;
int oldDepth = n.ptrDepth;
n.elementKind = resolved.primitiveKind;
n.ptrDepth = spec.ptrDepth;
if (n.refId != 0)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, 0}));
Q_UNUSED(oldEK); Q_UNUSED(oldDepth);
}
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
} else {
if (entry.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, entry.primitiveKind);
if (resolved.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, resolved.primitiveKind);
}
} else if (entry.entryKind == TypeEntry::Composite) {
} else if (resolved.entryKind == TypeEntry::Composite) {
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to composite type"));
@@ -2141,9 +2213,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (nodeKind != NodeKind::Pointer64)
changeNodeKind(nodeIdx, NodeKind::Pointer64);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId)
if (idx >= 0 && m_doc->tree.nodes[idx].refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
} else if (spec.arrayCount > 0) {
// Array modifier: e.g. "Material[10]" → Array + Struct element
@@ -2156,9 +2228,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
n.arrayLen, spec.arrayCount}));
if (n.refId != entry.structId)
if (n.refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, n.refId, entry.structId}));
cmd::ChangePointerRef{nodeId, n.refId, resolved.structId}));
}
} else {
@@ -2167,7 +2239,7 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
changeNodeKind(nodeIdx, NodeKind::Struct);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
int refIdx = m_doc->tree.indexOfId(entry.structId);
int refIdx = m_doc->tree.indexOfId(resolved.structId);
QString targetName;
if (refIdx >= 0) {
const Node& ref = m_doc->tree.nodes[refIdx];
@@ -2178,9 +2250,9 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
// Set refId so compose can expand the referenced struct's children
if (m_doc->tree.nodes[idx].refId != entry.structId)
if (m_doc->tree.nodes[idx].refId != resolved.structId)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, resolved.structId}));
// ChangePointerRef auto-sets collapsed=true when refId != 0
}
}
@@ -2190,28 +2262,28 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (!m_suppressRefresh) refresh();
}
} else if (mode == TypePopupMode::ArrayElement) {
if (entry.entryKind == TypeEntry::Primitive) {
if (entry.primitiveKind != elemKind) {
if (resolved.entryKind == TypeEntry::Primitive) {
if (resolved.primitiveKind != elemKind) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId,
elemKind, entry.primitiveKind,
elemKind, resolved.primitiveKind,
arrLen, arrLen}));
}
} else if (entry.entryKind == TypeEntry::Composite) {
if (elemKind != NodeKind::Struct || nodeRefId != entry.structId) {
} else if (resolved.entryKind == TypeEntry::Composite) {
if (elemKind != NodeKind::Struct || nodeRefId != resolved.structId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId,
elemKind, NodeKind::Struct,
arrLen, arrLen}));
if (nodeRefId != entry.structId) {
if (nodeRefId != resolved.structId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, nodeRefId, entry.structId}));
cmd::ChangePointerRef{nodeId, nodeRefId, resolved.structId}));
}
}
}
} else if (mode == TypePopupMode::PointerTarget) {
// "void" entry → refId 0; composite entry → real structId
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
uint64_t realRefId = (resolved.entryKind == TypeEntry::Composite) ? resolved.structId : 0;
if (realRefId != nodeRefId) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
@@ -2219,6 +2291,33 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
}
}
uint64_t RcxController::findOrCreateStructByName(const QString& typeName) {
// Check if it already exists locally
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& (n.structTypeName == typeName || (n.structTypeName.isEmpty() && n.name == typeName)))
return n.id;
}
// Import: create a new root struct with that name + default hex fields
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Import type"));
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0;
n.offset = 0;
n.id = m_doc->tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
for (int i = 0; i < 8; i++)
insertNode(n.id, i * 8, NodeKind::Hex64,
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
return n.id;
}
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
const auto* info = ProviderRegistry::instance().findProvider(providerIdentifier);
if (!info || !info->plugin) {
@@ -2268,6 +2367,117 @@ void RcxController::switchToSavedSource(int idx) {
}
}
void RcxController::selectSource(const QString& text) {
if (text == QStringLiteral("#clear")) {
clearSources();
} else if (text.startsWith(QStringLiteral("#saved:"))) {
int idx = text.mid(7).toInt();
switchToSavedSource(idx);
} else if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) {
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
m_doc->loadData(path);
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == QStringLiteral("File")
&& m_savedSources[i].filePath == path) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_doc->tree.baseAddress = m_savedSources[existingIdx].baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = QFileInfo(path).fileName();
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
}
} else {
const auto* providerInfo = ProviderRegistry::instance().findProvider(text.toLower().replace(" ", ""));
if (providerInfo) {
QString target;
bool selected = false;
if (providerInfo->isBuiltin) {
if (providerInfo->factory)
selected = providerInfo->factory(qobject_cast<QWidget*>(parent()), &target);
} else {
if (providerInfo->plugin)
selected = providerInfo->plugin->selectTarget(qobject_cast<QWidget*>(parent()), &target);
}
if (selected && !target.isEmpty()) {
std::unique_ptr<Provider> provider;
QString errorMsg;
if (providerInfo->plugin)
provider = providerInfo->plugin->createProvider(target, &errorMsg);
if (provider) {
if (m_activeSourceIdx >= 0 && m_activeSourceIdx < m_savedSources.size())
m_savedSources[m_activeSourceIdx].baseAddress = m_doc->tree.baseAddress;
uint64_t newBase = provider->base();
QString displayName = provider->name();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
m_doc->dataPath.clear();
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
resetSnapshot();
emit m_doc->documentChanged();
QString identifier = providerInfo->identifier;
int existingIdx = -1;
for (int i = 0; i < m_savedSources.size(); i++) {
if (m_savedSources[i].kind == identifier
&& m_savedSources[i].providerTarget == target) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_activeSourceIdx = existingIdx;
m_savedSources[existingIdx].baseAddress = m_doc->tree.baseAddress;
} else {
SavedSourceEntry entry;
entry.kind = identifier;
entry.displayName = displayName;
entry.providerTarget = target;
entry.baseAddress = m_doc->tree.baseAddress;
m_savedSources.append(entry);
m_activeSourceIdx = m_savedSources.size() - 1;
}
refresh();
} else if (!errorMsg.isEmpty()) {
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "Provider Error", errorMsg);
}
}
}
}
}
void RcxController::clearSources() {
m_savedSources.clear();
m_activeSourceIdx = -1;
m_doc->provider = std::make_shared<NullProvider>();
m_doc->dataPath.clear();
resetSnapshot();
pushSavedSourcesToEditors();
refresh();
}
void RcxController::pushSavedSourcesToEditors() {
QVector<SavedSourceDisplay> display;
display.reserve(m_savedSources.size());

View File

@@ -92,16 +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,
@@ -124,11 +128,16 @@ 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; }
@@ -165,13 +174,14 @@ private:
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

@@ -488,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);
}
});
@@ -806,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
@@ -1128,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 {
@@ -1515,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:
@@ -1786,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);
}
@@ -2368,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 ──
@@ -2455,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);
@@ -2466,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();
}

View File

@@ -69,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);

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

@@ -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;
}
@@ -347,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
applyFilter(m_filterEdit->text());
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
this, [this]() { updateModifierPreview(); });
@@ -516,22 +516,32 @@ void TypeSelectorPopup::setTitle(const QString& 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) {
@@ -541,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();
@@ -630,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) {
@@ -659,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

@@ -40,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
};
@@ -57,6 +58,7 @@ public:
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);

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

@@ -12,6 +12,14 @@
#include <QPainter>
#include <QCursor>
#include <QScreen>
#include <QMainWindow>
#include <QStatusBar>
#include <QPushButton>
#include <QButtonGroup>
#include <QLabel>
#include <QLayout>
#include <QHBoxLayout>
#include <QScrollBar>
#include <Qsci/qsciscintilla.h>
#include <Qsci/qsciscintillabase.h>
#include "editor.h"
@@ -2045,6 +2053,467 @@ private slots:
m_editor->applyDocument(m_result);
}
// ── Test: status bar view toggle buttons (pixel-level) ──
void testStatusBarViewToggleButtons() {
// Mirror the production ViewTabButton from main.cpp
static constexpr int kAccentH = 2;
static constexpr int kPadLR = 12;
static constexpr int kPadBot = 4;
class VTB : public QPushButton {
public:
QColor colBg, colBgChecked, colBgHover, colBgPressed;
QColor colText, colTextMuted, colAccent;
explicit VTB(const QString& t, QWidget* p = nullptr) : QPushButton(t, p) {
setCheckable(true); setFlat(true); setContentsMargins(0,0,0,0);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
}
QSize sizeHint() const override {
QFontMetrics fm(font());
return QSize(fm.horizontalAdvance(text()) + 2*kPadLR,
fm.height() + kAccentH + kPadBot);
}
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
QColor bg = colBg;
if (isDown()) bg = colBgPressed;
else if (underMouse()) bg = colBgHover;
else if (isChecked()) bg = colBgChecked;
p.fillRect(rect(), bg);
if (isChecked())
p.fillRect(0, 0, width(), kAccentH, colAccent);
p.setPen(isChecked() || underMouse() || isDown() ? colText : colTextMuted);
p.setFont(font());
QRect tr(kPadLR, kAccentH, width()-2*kPadLR, height()-kAccentH);
p.drawText(tr, Qt::AlignVCenter|Qt::AlignLeft, text());
}
void enterEvent(QEnterEvent*) override { update(); }
void leaveEvent(QEvent*) override { update(); }
};
QColor bg(30,30,30), bgAlt(45,45,48), hover(62,62,66);
QColor text(212,212,212), textMuted(128,128,128);
QColor accent("#b180d7");
QColor pressed = hover.darker(130);
auto setColors = [&](VTB* b) {
b->colBg = bg; b->colBgChecked = bgAlt; b->colBgHover = hover;
b->colBgPressed = pressed; b->colText = text;
b->colTextMuted = textMuted; b->colAccent = accent;
};
// Borderless status bar with manual layout (mirrors production FlatStatusBar)
class FSB : public QStatusBar {
public:
QWidget* tabRow = nullptr;
QLabel* label = nullptr;
FSB() { setSizeGripEnabled(false); }
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this); p.fillRect(rect(), palette().window());
}
void resizeEvent(QResizeEvent* e) override {
QStatusBar::resizeEvent(e);
doLayout();
}
void showEvent(QShowEvent* e) override {
QStatusBar::showEvent(e);
doLayout();
}
private:
void doLayout() {
if (!tabRow || !label) return;
int h = height(), tw = tabRow->sizeHint().width();
tabRow->setGeometry(0, 0, tw, h);
label->setGeometry(tw, 0, width() - tw, h);
}
};
QMainWindow win;
win.resize(600, 400);
QPalette pal; pal.setColor(QPalette::Window, bg);
win.setPalette(pal);
auto* sb = new FSB;
win.setStatusBar(sb);
sb->setPalette(pal);
sb->setAutoFillBackground(true);
if (win.layout()) {
win.layout()->setSpacing(0);
win.layout()->setContentsMargins(0,0,0,0);
}
auto* btnGroup = new QButtonGroup(&win);
btnGroup->setExclusive(true);
auto* btnR = new VTB("Reclass");
auto* btnC = new VTB("C/C++");
setColors(btnR); setColors(btnC);
btnR->setChecked(true);
btnGroup->addButton(btnR, 0);
btnGroup->addButton(btnC, 1);
auto* tabRow = new QWidget(sb);
auto* tabLay = new QHBoxLayout(tabRow);
tabLay->setContentsMargins(0,0,0,0);
tabLay->setSpacing(0);
tabLay->addWidget(btnR);
tabLay->addWidget(btnC);
auto* lbl = new QLabel("Ready", sb);
lbl->setContentsMargins(10,0,0,0);
sb->tabRow = tabRow;
sb->label = lbl;
win.show();
QVERIFY(QTest::qWaitForWindowExposed(&win));
QTest::qWait(100);
// ── Toggle logic ──
QVERIFY(btnR->isChecked());
QVERIFY(!btnC->isChecked());
QTest::mouseClick(btnC, Qt::LeftButton);
QVERIFY(btnC->isChecked());
QVERIFY(!btnR->isChecked());
QTest::mouseClick(btnR, Qt::LeftButton);
QVERIFY(btnR->isChecked());
QTest::qWait(50);
// ── Pixel: accent line on checked button at rows 0..(kAccentH-1) ──
QImage imgR = btnR->grab().toImage().convertToFormat(QImage::Format_ARGB32);
QVERIFY(imgR.height() >= kAccentH + 4);
// Every pixel in the top kAccentH rows (middle 80% width) must be accent
int x0 = imgR.width() / 10, x1 = imgR.width() * 9 / 10;
for (int y = 0; y < kAccentH; y++) {
for (int x = x0; x < x1; x++) {
QColor c(imgR.pixel(x, y));
QVERIFY2(qAbs(c.red() - accent.red()) < 10
&& qAbs(c.green() - accent.green()) < 10
&& qAbs(c.blue() - accent.blue()) < 10,
qPrintable(QString("Checked btn pixel(%1,%2)=%3 expected accent %4")
.arg(x).arg(y).arg(c.name(), accent.name())));
}
}
// Mid-height row must NOT be accent (accent doesn't bleed into body)
{
int midY = imgR.height() / 2;
QColor c(imgR.pixel(imgR.width()/2, midY));
QVERIFY2(qAbs(c.red() - accent.red()) > 15
|| qAbs(c.green() - accent.green()) > 15
|| qAbs(c.blue() - accent.blue()) > 15,
qPrintable(QString("Row %1 should be background, not accent: %2")
.arg(midY).arg(c.name())));
}
// ── Pixel: unchecked button has NO accent line ──
QImage imgC = btnC->grab().toImage().convertToFormat(QImage::Format_ARGB32);
for (int y = 0; y < kAccentH; y++) {
QColor c(imgC.pixel(imgC.width()/2, y));
QVERIFY2(qAbs(c.red() - accent.red()) > 15
|| qAbs(c.green() - accent.green()) > 15
|| qAbs(c.blue() - accent.blue()) > 15,
qPrintable(QString("Unchecked btn row %1 has accent: %2")
.arg(y).arg(c.name())));
}
// ── Pixel: zero gap between the two buttons ──
// Map to their shared parent (the tabRow container)
QWidget* container = btnR->parentWidget();
int rRight = btnR->mapTo(container, QPoint(btnR->width(), 0)).x();
int cLeft = btnC->mapTo(container, QPoint(0, 0)).x();
QVERIFY2(rRight == cLeft,
qPrintable(QString("Gap between buttons: btnR right=%1 btnC left=%2 gap=%3")
.arg(rRight).arg(cLeft).arg(cLeft - rRight)));
// ── Pressed color is darker than hover ──
QVERIFY2(pressed.lightness() < hover.lightness(),
qPrintable(QString("Pressed %1 should be darker than hover %2")
.arg(pressed.name(), hover.name())));
// ── Button starts at x=0 in status bar (no left padding) ──
QPoint btnTopLeft = tabRow->mapTo(sb, QPoint(0, 0));
QVERIFY2(btnTopLeft.x() == 0,
qPrintable(QString("Tab row left margin: x=%1, expected 0").arg(btnTopLeft.x())));
// ── Button starts at y=0 in status bar (no top padding) ──
QVERIFY2(btnTopLeft.y() == 0,
qPrintable(QString("Tab row top margin: y=%1, expected 0").arg(btnTopLeft.y())));
// ── Button takes full status bar height ──
QVERIFY2(btnR->height() == sb->height(),
qPrintable(QString("Button height=%1 sb height=%2")
.arg(btnR->height()).arg(sb->height())));
// ── Accent at y=0 in status bar pixel coordinates (grab status bar) ──
QImage sbImg = sb->grab().toImage().convertToFormat(QImage::Format_ARGB32);
{
QColor c(sbImg.pixel(btnR->width()/2, 0));
QVERIFY2(qAbs(c.red() - accent.red()) < 10
&& qAbs(c.green() - accent.green()) < 10
&& qAbs(c.blue() - accent.blue()) < 10,
qPrintable(QString("Status bar pixel(x,%1,0)=%2 expected accent %3")
.arg(btnR->width()/2).arg(c.name(), accent.name())));
}
qDebug() << QString("ViewTabButton: accent=%1 btnH=%2 sbH=%3 gap=%4 leftX=%5 topY=%6")
.arg(accent.name()).arg(btnR->height()).arg(sb->height())
.arg(cLeft - rRight).arg(btnTopLeft.x()).arg(btnTopLeft.y());
}
// ── Test: resize grip dots are equidistant from right and bottom window edges ──
// The grip is a direct child of the window positioned via move(), not inside
// the status bar layout. This test verifies the dot placement is symmetric
// regardless of font, and runs the check at two different font sizes to prove
// font independence.
// ── Test: horizontal scrollbar after long name rename ──
void testHScrollResetAfterNameShrink() {
// Use a dedicated narrow editor so content easily overflows the viewport
auto* editor = new RcxEditor();
editor->resize(200, 300);
editor->show();
QVERIFY(QTest::qWaitForWindowExposed(editor));
auto* sci = editor->scintilla();
auto* hbar = sci->horizontalScrollBar();
auto makeTree = [](const QString& fieldName) {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "MyStruct";
root.name = "s";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f;
f.kind = NodeKind::Int32;
f.name = fieldName;
f.parentId = rootId;
f.offset = 0;
tree.addNode(f);
return tree;
};
BufferProvider prov(QByteArray(64, '\0'));
// ── Step 1: long name → wide content, scrollbar must appear ──
QString longName = QString(120, QChar('W'));
{
NodeTree tree = makeTree(longName);
ComposeResult cr = compose(tree, prov);
editor->applyDocument(cr);
QApplication::processEvents();
QTest::qWait(50);
}
int scrollW1 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
int viewW = sci->viewport()->width();
qDebug() << QString("Long name: scrollW=%1 vpW=%2 hbar.visible=%3 "
"hbar.max=%4 hbar.value=%5")
.arg(scrollW1).arg(viewW)
.arg(hbar->isVisible())
.arg(hbar->maximum()).arg(hbar->value());
QVERIFY2(scrollW1 > viewW,
qPrintable(QString("scrollW=%1 should exceed vpW=%2")
.arg(scrollW1).arg(viewW)));
// Scrollbar must be visible when content overflows
QVERIFY2(hbar->isVisible(),
"Horizontal scrollbar should be visible when content overflows");
QVERIFY2(hbar->maximum() > 0,
qPrintable(QString("Scrollbar max should be >0, got %1")
.arg(hbar->maximum())));
// Simulate user scrolled right
sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)(scrollW1 / 2));
QApplication::processEvents();
QTest::qWait(20);
int xOff1 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
QVERIFY2(xOff1 > 0, "X offset should be non-zero after scrolling right");
// ── Step 2: short name → narrower content ──
{
NodeTree tree = makeTree("x");
ComposeResult cr = compose(tree, prov);
editor->applyDocument(cr);
QApplication::processEvents();
QTest::qWait(50);
}
int scrollW2 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
int xOff2 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
qDebug() << QString("Short name: scrollW=%1 xOff=%2 vpW=%3 hbar.visible=%4 "
"hbar.max=%5 hbar.value=%6")
.arg(scrollW2).arg(xOff2).arg(viewW)
.arg(hbar->isVisible())
.arg(hbar->maximum()).arg(hbar->value());
// Scroll width should have shrunk
QVERIFY2(scrollW2 < scrollW1,
qPrintable(QString("scrollW should shrink: was %1, now %2")
.arg(scrollW1).arg(scrollW2)));
// X offset must be clamped to max(0, scrollW - viewportW)
int maxValidXOff = qMax(0, scrollW2 - viewW);
QVERIFY2(xOff2 <= maxValidXOff,
qPrintable(QString("xOffset=%1 exceeds max valid=%2 (scrollW=%3 vpW=%4)")
.arg(xOff2).arg(maxValidXOff).arg(scrollW2).arg(viewW)));
// If content fits viewport entirely, offset must be 0
if (scrollW2 <= viewW) {
QCOMPARE(xOff2, 0);
}
// If content still overflows, scrollbar must still be visible
if (scrollW2 > viewW) {
QVERIFY2(hbar->isVisible(),
"Scrollbar should remain visible when content still overflows");
}
// ── Step 3: apply long name again → scrollbar must reappear ──
{
NodeTree tree = makeTree(longName);
ComposeResult cr = compose(tree, prov);
editor->applyDocument(cr);
QApplication::processEvents();
QTest::qWait(50);
}
int scrollW3 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
int xOff3 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
qDebug() << QString("Long again: scrollW=%1 xOff=%2 hbar.visible=%3 hbar.max=%4")
.arg(scrollW3).arg(xOff3)
.arg(hbar->isVisible()).arg(hbar->maximum());
QVERIFY2(scrollW3 > viewW,
qPrintable(QString("scrollW=%1 should exceed vpW=%2 after re-widen")
.arg(scrollW3).arg(viewW)));
QVERIFY2(hbar->isVisible(),
"Scrollbar must reappear after content widens again");
// After fresh apply with no prior scroll, xOffset should be 0
QCOMPARE(xOff3, 0);
delete editor;
}
void testResizeGripCornerSymmetry() {
// Same constants as production ResizeGrip in main.cpp
static constexpr int kSize = 16;
static constexpr int kPad = 4;
static constexpr double kInset = 4.0;
class Grip : public QWidget {
public:
explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(kSize, kSize); }
void reposition() {
if (auto* w = parentWidget())
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(Qt::red);
const double r = 1.0, s = 4.0;
double bx = width() - kInset;
double by = height() - kInset;
p.drawEllipse(QPointF(bx, by), r, r);
p.drawEllipse(QPointF(bx - s, by), r, r);
p.drawEllipse(QPointF(bx - 2 * s, by), r, r);
p.drawEllipse(QPointF(bx, by - s), r, r);
p.drawEllipse(QPointF(bx - s, by - s), r, r);
p.drawEllipse(QPointF(bx, by - 2 * s), r, r);
}
};
// Helper: grab window, find bottommost-rightmost red pixel, measure gaps
auto measureGaps = [](QWidget* win, int& gapRight, int& gapBottom) -> bool {
QPixmap px = win->grab();
QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32);
int W = img.width(), H = img.height();
if (W < 50 || H < 50) return false;
int foundX = -1, foundY = -1;
for (int y = H - 1; y >= H - 40 && foundY < 0; --y) {
for (int x = W - 1; x >= W - 40; --x) {
QColor c(img.pixel(x, y));
if (c.red() > 180 && c.green() < 80 && c.blue() < 80) {
foundX = x; foundY = y; break;
}
}
}
if (foundX < 0) return false;
gapRight = (W - 1) - foundX;
gapBottom = (H - 1) - foundY;
// Save diagnostic image
QImage diag = img.copy();
QPainter dp(&diag);
dp.setPen(QPen(Qt::cyan, 1));
dp.drawRect(foundX - 3, foundY - 3, 6, 6);
dp.setPen(QPen(Qt::yellow, 1));
dp.drawLine(foundX, foundY, W - 1, foundY);
dp.drawLine(foundX, foundY, foundX, H - 1);
dp.end();
diag.save("grip_corner_diag.png");
return true;
};
// --- Round 1: default system font ---
QMainWindow win;
win.resize(500, 375);
QPalette pal;
pal.setColor(QPalette::Window, QColor(30, 30, 30));
win.setPalette(pal);
win.statusBar()->setPalette(pal);
win.statusBar()->setAutoFillBackground(true);
auto* grip = new Grip(&win);
grip->raise();
win.show();
QVERIFY(QTest::qWaitForWindowExposed(&win));
grip->reposition();
QTest::qWait(100);
int gapR1 = 0, gapB1 = 0;
QVERIFY2(measureGaps(&win, gapR1, gapB1),
"Could not find red grip dot (round 1)");
QVERIFY2(gapR1 == gapB1,
qPrintable(QString("Round 1 asymmetric: gapRight=%1 gapBottom=%2")
.arg(gapR1).arg(gapB1)));
// --- Round 2: large font on status bar (must NOT change grip position) ---
QFont bigFont("Arial", 24);
win.statusBar()->setFont(bigFont);
QTest::qWait(100);
grip->reposition();
QTest::qWait(100);
int gapR2 = 0, gapB2 = 0;
QVERIFY2(measureGaps(&win, gapR2, gapB2),
"Could not find red grip dot (round 2, big font)");
QVERIFY2(gapR2 == gapB2,
qPrintable(QString("Round 2 asymmetric: gapRight=%1 gapBottom=%2")
.arg(gapR2).arg(gapB2)));
// Gaps must be identical across both font sizes
QVERIFY2(gapR1 == gapR2 && gapB1 == gapB2,
qPrintable(QString("Font changed grip position: "
"round1=(%1,%2) round2=(%3,%4)")
.arg(gapR1).arg(gapB1).arg(gapR2).arg(gapB2)));
qDebug() << "Grip corner symmetry:"
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
.arg(gapR1).arg(gapB1);
}
};
QTEST_MAIN(TestEditor)

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>
@@ -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() {
@@ -960,6 +963,508 @@ private slots:
// 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"