mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
18 Commits
msvc-fix-2
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596f410b96 | ||
|
|
f0fc85f60f | ||
|
|
70c7404556 | ||
|
|
f27459c21b | ||
|
|
a5abcbeea6 | ||
|
|
7071402319 | ||
|
|
0dc390ed86 | ||
|
|
188c27c6e2 | ||
|
|
81f1e4319f | ||
|
|
3ab6affa5e | ||
|
|
35b3cd9ac1 | ||
|
|
e5938f7e82 | ||
|
|
03c49d19dd | ||
|
|
b7eebedf50 | ||
|
|
9ff456a8d6 | ||
|
|
580f285edd | ||
|
|
d23a6c7656 | ||
|
|
25d8de95b7 |
102
CMakeLists.txt
102
CMakeLists.txt
@@ -22,6 +22,32 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
||||
set(QT Qt${QT_VERSION_MAJOR})
|
||||
message(STATUS "Using ${QT}: ${${QT}_DIR}")
|
||||
|
||||
# ── ABI sanity check: prevent MSVC ↔ MinGW Qt mismatch ──
|
||||
# Building with MSVC against MinGW Qt (or vice versa) compiles fine but
|
||||
# crashes immediately at runtime (ABI mismatch in QString/QSettings internals).
|
||||
if(MSVC AND "${${QT}_DIR}" MATCHES "mingw")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MinGW but this project is being compiled with MSVC.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MSVC (e.g. msvc2019_64) and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/msvc2019_64 ..")
|
||||
elseif(MINGW AND "${${QT}_DIR}" MATCHES "msvc")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MSVC but this project is being compiled with MinGW.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MinGW and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/mingw_64 ..")
|
||||
endif()
|
||||
|
||||
# ── MSVC compile flags ──
|
||||
if(MSVC)
|
||||
# /utf-8: treat source and execution character sets as UTF-8
|
||||
# /MP: multi-processor compilation
|
||||
add_compile_options(/utf-8 /MP)
|
||||
endif()
|
||||
|
||||
# Qt5 on Windows needs WinExtras for HICON conversion
|
||||
set(_QT_WINEXTRAS "")
|
||||
if(QT_VERSION_MAJOR EQUAL 5 AND WIN32)
|
||||
@@ -151,6 +177,26 @@ target_link_libraries(Reclass PRIVATE
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
|
||||
|
||||
# Copy Debugging Tools dbghelp.dll next to Reclass.exe so the Windows
|
||||
# loader picks it up (app dir > System32). The system dbghelp.dll
|
||||
# lacks StackWalk2 which the tools dbgeng.dll needs for remote debug.
|
||||
set(_DBG_TOOLS_DIRS
|
||||
"C:/Program Files (x86)/Windows Kits/10/Debuggers/x64"
|
||||
"C:/Program Files/Windows Kits/10/Debuggers/x64")
|
||||
foreach(_dir ${_DBG_TOOLS_DIRS})
|
||||
if(EXISTS "${_dir}/dbghelp.dll")
|
||||
foreach(_dll dbghelp.dll dbgcore.dll symsrv.dll)
|
||||
if(EXISTS "${_dir}/${_dll}")
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_dir}/${_dll}" $<TARGET_FILE_DIR:Reclass>
|
||||
COMMENT "Copying ${_dll} from Debugging Tools")
|
||||
endif()
|
||||
endforeach()
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||
@@ -164,12 +210,30 @@ if(APPLE)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Copy built-in theme JSON files to build directory
|
||||
# Copy built-in theme JSON files next to the executable.
|
||||
# For single-config generators (Ninja/Make) the exe is in ${CMAKE_BINARY_DIR},
|
||||
# for multi-config generators (MSVC/Xcode) it's in ${CMAKE_BINARY_DIR}/<config>.
|
||||
# Using a post-build copy with $<TARGET_FILE_DIR:Reclass> handles both.
|
||||
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||
|
||||
# Single-config: configure_file for IDE convenience (available before first build)
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Post-build: always copy to the actual exe directory (works for all generators)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/themes"
|
||||
COMMENT "Creating themes directory next to executable")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_tf}" "$<TARGET_FILE_DIR:Reclass>/themes/${_name}")
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
@@ -178,12 +242,25 @@ if(APPLE)
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
# Copy example .rcx files next to the executable (same logic as themes)
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/examples"
|
||||
COMMENT "Creating examples directory next to executable")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_ef}" "$<TARGET_FILE_DIR:Reclass>/examples/${_name}")
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
@@ -346,11 +423,6 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(grab_tabs tests/grab_tabs.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp src/resources.qrc)
|
||||
target_include_directories(grab_tabs PRIVATE src)
|
||||
target_link_libraries(grab_tabs PRIVATE ${QT}::Widgets ${QT}::Svg ${QT}::Test)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
@@ -505,6 +577,14 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
|
||||
add_executable(bench_project tests/bench_project.cpp)
|
||||
target_include_directories(bench_project PRIVATE src)
|
||||
target_link_libraries(bench_project PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_project PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_project COMMAND bench_project)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# cmake/deploy.cmake - Dual-mode script for deploying Qt runtime DLLs
|
||||
#
|
||||
# Script mode: cmake -P deploy.cmake <target_exe> <windeployqt>
|
||||
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target)
|
||||
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target + post-build)
|
||||
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
set(TARGET_EXE ${CMAKE_ARGV3})
|
||||
@@ -17,7 +17,6 @@ if(CMAKE_SCRIPT_MODE_FILE)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${WINDEPLOYQT}
|
||||
--pdb
|
||||
--no-compiler-runtime
|
||||
--no-translations
|
||||
--no-opengl-sw
|
||||
@@ -67,6 +66,7 @@ if(NOT TARGET ${QT}::windeployqt AND TARGET ${QT}::qmake)
|
||||
endif()
|
||||
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
# Standalone "deploy" target (can still be invoked manually)
|
||||
add_custom_target(deploy
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
@@ -79,4 +79,13 @@ if(TARGET ${QT}::windeployqt)
|
||||
set_target_properties(deploy PROPERTIES
|
||||
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
|
||||
)
|
||||
|
||||
# Auto-deploy as post-build step so the correct Qt DLLs are always next
|
||||
# to the exe. Without this, MSVC builds load whatever Qt DLLs happen to
|
||||
# be in PATH (often MinGW ones), causing instant ABI-mismatch crashes.
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
$<TARGET_FILE:${QT}::windeployqt>
|
||||
COMMENT "Auto-deploying Qt runtime DLLs...")
|
||||
endif()
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
@@ -84,7 +85,8 @@
|
||||
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe</Command>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
|
||||
@@ -19,6 +19,60 @@
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
typedef struct _UNICODE_STRING { USHORT Length, MaximumLength; PWSTR Buffer; } UNICODE_STRING;
|
||||
typedef struct _CLIENT_ID { HANDLE UniqueProcess; HANDLE UniqueThread; } CLIENT_ID;
|
||||
typedef struct _SYSTEM_THREAD_INFORMATION {
|
||||
LARGE_INTEGER KernelTime, UserTime, CreateTime;
|
||||
ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId;
|
||||
LONG Priority, BasePriority; ULONG ContextSwitches, ThreadState, WaitReason;
|
||||
} SYSTEM_THREAD_INFORMATION;
|
||||
typedef struct _SYSTEM_PROCESS_INFORMATION {
|
||||
ULONG NextEntryOffset; // 0x000
|
||||
ULONG NumberOfThreads; // 0x004
|
||||
LARGE_INTEGER WorkingSetPrivateSize; // 0x008
|
||||
ULONG HardFaultCount; // 0x010
|
||||
ULONG NumberOfThreadsHighWatermark; // 0x014
|
||||
ULONGLONG CycleTime; // 0x018
|
||||
LARGE_INTEGER CreateTime; // 0x020
|
||||
LARGE_INTEGER UserTime; // 0x028
|
||||
LARGE_INTEGER KernelTime; // 0x030
|
||||
UNICODE_STRING ImageName; // 0x038
|
||||
LONG BasePriority; // 0x048
|
||||
HANDLE UniqueProcessId; // 0x050
|
||||
PVOID InheritedFromUniqueProcessId; // 0x058
|
||||
ULONG HandleCount; // 0x060
|
||||
ULONG SessionId; // 0x064
|
||||
ULONG_PTR UniqueProcessKey; // 0x068
|
||||
SIZE_T PeakVirtualSize; // 0x070
|
||||
SIZE_T VirtualSize; // 0x078
|
||||
ULONG PageFaultCount; // 0x080
|
||||
ULONG _pad0; // 0x084
|
||||
SIZE_T PeakWorkingSetSize; // 0x088
|
||||
SIZE_T WorkingSetSize; // 0x090
|
||||
SIZE_T QuotaPeakPagedPoolUsage; // 0x098
|
||||
SIZE_T QuotaPagedPoolUsage; // 0x0A0
|
||||
SIZE_T QuotaPeakNonPagedPoolUsage; // 0x0A8
|
||||
SIZE_T QuotaNonPagedPoolUsage; // 0x0B0
|
||||
SIZE_T PagefileUsage; // 0x0B8
|
||||
SIZE_T PeakPagefileUsage; // 0x0C0
|
||||
SIZE_T PrivatePageCount; // 0x0C8
|
||||
LARGE_INTEGER ReadOperationCount; // 0x0D0
|
||||
LARGE_INTEGER WriteOperationCount; // 0x0D8
|
||||
LARGE_INTEGER OtherOperationCount; // 0x0E0
|
||||
LARGE_INTEGER ReadTransferCount; // 0x0E8
|
||||
LARGE_INTEGER WriteTransferCount; // 0x0F0
|
||||
LARGE_INTEGER OtherTransferCount; // 0x0F8
|
||||
} SYSTEM_PROCESS_INFORMATION; // sizeof = 0x100
|
||||
typedef struct alignas(8) _THREAD_BASIC_INFORMATION {
|
||||
NTSTATUS ExitStatus; // 0x00
|
||||
ULONG _pad; // 0x04
|
||||
PVOID TebBaseAddress; // 0x08
|
||||
CLIENT_ID ClientId; // 0x10
|
||||
ULONG_PTR AffinityMask; // 0x20
|
||||
LONG Priority; // 0x28
|
||||
LONG BasePriority; // 0x2C
|
||||
} THREAD_BASIC_INFORMATION;
|
||||
#elif defined(__linux__)
|
||||
#include <climits>
|
||||
#include <sys/types.h>
|
||||
@@ -61,6 +115,17 @@ ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& proces
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(m_handle, &isWow64) && isWow64)
|
||||
m_pointerSize = 4;
|
||||
// Query PEB address via NtQueryInformationProcess
|
||||
{
|
||||
typedef NTSTATUS(NTAPI* NtQIP_t)(HANDLE, ULONG, PVOID, ULONG, PULONG);
|
||||
static NtQIP_t pNtQIP = (NtQIP_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
|
||||
if (pNtQIP) {
|
||||
struct { PVOID r1; PVOID PebBaseAddress; PVOID r2[2]; ULONG_PTR pid; PVOID r3; } pbi = {};
|
||||
ULONG retLen = 0;
|
||||
if (pNtQIP(m_handle, /*ProcessBasicInformation*/0, &pbi, sizeof(pbi), &retLen) >= 0 && pbi.PebBaseAddress)
|
||||
m_peb = (uint64_t)(uintptr_t)pbi.PebBaseAddress;
|
||||
}
|
||||
}
|
||||
cacheModules();
|
||||
}
|
||||
}
|
||||
@@ -426,6 +491,58 @@ int ProcessMemoryProvider::size() const
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
QVector<rcx::Provider::ThreadInfo> ProcessMemoryProvider::tebs() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
QVector<ThreadInfo> result;
|
||||
if (!m_handle || !m_peb) return result;
|
||||
|
||||
typedef NTSTATUS(NTAPI* NtQSI_t)(ULONG, PVOID, ULONG, PULONG);
|
||||
typedef NTSTATUS(NTAPI* NtQIT_t)(HANDLE, ULONG, PVOID, ULONG, PULONG);
|
||||
static auto pNtQSI = (NtQSI_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
|
||||
static auto pNtQIT = (NtQIT_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationThread");
|
||||
if (!pNtQSI || !pNtQIT) return result;
|
||||
|
||||
// Enumerate threads via SystemProcessInformation (class 5)
|
||||
ULONG retLen = 0;
|
||||
ULONG bufSize = 1 << 20;
|
||||
QByteArray buf(bufSize, 0);
|
||||
NTSTATUS qsiSt;
|
||||
for (int attempt = 0; attempt < 8; ++attempt) {
|
||||
qsiSt = pNtQSI(5, buf.data(), bufSize, &retLen);
|
||||
if ((uint32_t)qsiSt != 0xC0000004u) break;
|
||||
bufSize *= 2;
|
||||
buf.resize(bufSize);
|
||||
}
|
||||
if (qsiSt < 0) return result;
|
||||
|
||||
// Walk process entries to find ours
|
||||
auto* proc = (SYSTEM_PROCESS_INFORMATION*)buf.data();
|
||||
for (;;) {
|
||||
if ((uintptr_t)proc->UniqueProcessId == m_pid) {
|
||||
auto* threads = (SYSTEM_THREAD_INFORMATION*)((char*)proc + sizeof(*proc));
|
||||
for (ULONG i = 0; i < proc->NumberOfThreads; ++i) {
|
||||
DWORD tid = (DWORD)(uintptr_t)threads[i].ClientId.UniqueThread;
|
||||
HANDLE hThread = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid);
|
||||
if (!hThread) continue;
|
||||
THREAD_BASIC_INFORMATION tbi = {};
|
||||
ULONG tbiLen = 0;
|
||||
NTSTATUS qitSt = pNtQIT(hThread, 0, &tbi, sizeof(tbi), &tbiLen);
|
||||
if (qitSt >= 0 && tbi.TebBaseAddress)
|
||||
result.append({(uint64_t)(uintptr_t)tbi.TebBaseAddress, tid});
|
||||
CloseHandle(hThread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!proc->NextEntryOffset) break;
|
||||
proc = (SYSTEM_PROCESS_INFORMATION*)((char*)proc + proc->NextEntryOffset);
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -41,6 +41,8 @@ public:
|
||||
// Process-specific helpers
|
||||
uint32_t pid() const { return m_pid; }
|
||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||
uint64_t peb() const override { return m_peb; }
|
||||
QVector<ThreadInfo> tebs() const override;
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
@@ -56,6 +58,7 @@ private:
|
||||
bool m_writable;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
uint64_t m_peb = 0;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
|
||||
@@ -20,7 +20,7 @@ set(PLUGIN_SOURCES
|
||||
add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt + DbgEng
|
||||
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets dbgeng ole32)
|
||||
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets ole32)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(WinDbgMemoryPlugin PRIVATE
|
||||
|
||||
@@ -12,12 +12,99 @@
|
||||
#include <QDebug>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#pragma comment(lib, "dbgeng.lib")
|
||||
// dbgeng.dll is loaded dynamically — see loadDbgEngTools()
|
||||
|
||||
// The system dbgeng.dll (C:\Windows\System32) does not support remote
|
||||
// connections (DebugConnect returns 0x8007053d). The full version lives
|
||||
// in the Debugging Tools for Windows directory. We load it dynamically
|
||||
// so the plugin works without requiring the debugger tools on PATH.
|
||||
static const char* const kDbgToolsDirs[] = {
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64",
|
||||
"C:\\Program Files\\Windows Kits\\10\\Debuggers\\x64",
|
||||
};
|
||||
static const char* const kSettingsKey = "WinDbgPlugin/DbgToolsDir";
|
||||
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugConnect)(PCSTR, REFIID, PVOID*);
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugCreate)(REFIID, PVOID*);
|
||||
|
||||
static QString s_loadedDir;
|
||||
static HMODULE s_hDbgEng = nullptr;
|
||||
|
||||
static HMODULE tryLoadFrom(const char* dir) {
|
||||
SetDllDirectoryA(dir);
|
||||
// Pre-load dependencies so the tools versions are used instead of
|
||||
// the older System32 copies (e.g. dbghelp.dll without StackWalk2).
|
||||
char path[MAX_PATH];
|
||||
for (auto dep : {"dbghelp.dll", "dbgcore.dll", "symsrv.dll"}) {
|
||||
snprintf(path, sizeof(path), "%s\\%s", dir, dep);
|
||||
LoadLibraryA(path); // OK if missing
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s\\dbgeng.dll", dir);
|
||||
HMODULE h = LoadLibraryA(path);
|
||||
if (h) {
|
||||
s_loadedDir = QString::fromLocal8Bit(dir);
|
||||
qDebug() << "[WinDbg] Loaded dbgeng.dll from" << dir;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static HMODULE loadDbgEngTools() {
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
|
||||
// 1. Try user-configured path from settings
|
||||
QSettings settings;
|
||||
QString userDir = settings.value(kSettingsKey).toString();
|
||||
if (!userDir.isEmpty()) {
|
||||
s_hDbgEng = tryLoadFrom(userDir.toLocal8Bit().constData());
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
// 2. Try well-known install paths
|
||||
for (auto dir : kDbgToolsDirs) {
|
||||
s_hDbgEng = tryLoadFrom(dir);
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
SetDllDirectoryA(nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool dbgToolsFound() {
|
||||
loadDbgEngTools();
|
||||
return s_hDbgEng != nullptr;
|
||||
}
|
||||
|
||||
static PFN_DebugConnect getDebugConnect() {
|
||||
static PFN_DebugConnect pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugConnect)GetProcAddress(h, "DebugConnect");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugConnect not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
|
||||
static PFN_DebugCreate getDebugCreate() {
|
||||
static PFN_DebugCreate pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugCreate)GetProcAddress(h, "DebugCreate");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugCreate not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -65,6 +152,9 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
dispatchToOwner([this, &target]() {
|
||||
HRESULT hr;
|
||||
|
||||
// COM must be initialized on this thread for DbgEng
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
|
||||
qDebug() << "[WinDbg] Opening target:" << target
|
||||
<< "on DbgEng thread" << QThread::currentThread();
|
||||
|
||||
@@ -72,9 +162,11 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||
{
|
||||
// ── Remote: connect to existing WinDbg debug server ──
|
||||
auto pfnConnect = getDebugConnect();
|
||||
if (!pfnConnect) { qWarning() << "[WinDbg] Debugging Tools required for remote connections"; return; }
|
||||
QByteArray connUtf8 = target.toUtf8();
|
||||
qDebug() << "[WinDbg] DebugConnect:" << target;
|
||||
hr = DebugConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||
hr = pfnConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
@@ -86,7 +178,9 @@ WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
else
|
||||
{
|
||||
// ── Local: create debug client for pid/dump ──
|
||||
hr = DebugCreate(IID_IDebugClient, (void**)&m_client);
|
||||
auto pfnCreate = getDebugCreate();
|
||||
if (!pfnCreate) { qWarning() << "[WinDbg] Debugging Tools required"; return; }
|
||||
hr = pfnCreate(IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
@@ -239,6 +333,7 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||
m_client->DetachProcesses();
|
||||
}
|
||||
cleanup();
|
||||
CoUninitialize();
|
||||
});
|
||||
} else {
|
||||
// Thread not running — clean up directly (best-effort)
|
||||
@@ -503,7 +598,7 @@ std::unique_ptr<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString&
|
||||
*errorMsg = QString("Failed to connect to debug server.\n\n"
|
||||
"Target: %1\n\n"
|
||||
"Make sure WinDbg is running with a matching .server command\n"
|
||||
"(e.g. .server tcp:port=5055) and the port/pipe is reachable.")
|
||||
"(e.g. .server tcp:port=5056) and the port/pipe is reachable.")
|
||||
.arg(target);
|
||||
else if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||
*errorMsg = QString("Failed to attach to process.\n\n"
|
||||
@@ -532,7 +627,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
QDialog dlg(parent);
|
||||
dlg.setWindowTitle("WinDbg Settings");
|
||||
dlg.resize(460, 300);
|
||||
dlg.resize(480, 360);
|
||||
|
||||
QPalette dlgPal = qApp->palette();
|
||||
dlg.setPalette(dlgPal);
|
||||
@@ -540,17 +635,27 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
|
||||
QColor editBg = dlgPal.window().color().darker(115);
|
||||
QString editSS = QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" border-radius: 3px; padding: 4px 6px; }")
|
||||
.arg(editBg.name(),
|
||||
dlgPal.color(QPalette::Text).name(),
|
||||
dlgPal.color(QPalette::Mid).name());
|
||||
|
||||
layout->addWidget(new QLabel(
|
||||
"Connect to a running WinDbg debug server.\n"
|
||||
"In WinDbg, run: .server tcp:port=5055\n\n"
|
||||
"In WinDbg, run: .server tcp:port=5056\n\n"
|
||||
"Non-invasive debug and dump files only.\n"
|
||||
"Execution control (bp, g, t, p) is not supported."));
|
||||
"Execution control (bp, g, t, p) is not supported.\n"
|
||||
"WinDbg Classic is recommended."));
|
||||
|
||||
layout->addSpacing(8);
|
||||
layout->addWidget(new QLabel("Connection string:"));
|
||||
auto* connEdit = new QLineEdit;
|
||||
connEdit->setPlaceholderText("tcp:Port=5055,Server=localhost");
|
||||
connEdit->setText("tcp:Port=5055,Server=localhost");
|
||||
connEdit->setPlaceholderText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setStyleSheet(editSS);
|
||||
layout->addWidget(connEdit);
|
||||
|
||||
layout->addSpacing(4);
|
||||
@@ -574,8 +679,72 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
layout->addLayout(row);
|
||||
};
|
||||
|
||||
addExample(".server tcp:port=5055");
|
||||
addExample(".server tcp:port=5056");
|
||||
addExample(".server npipe:pipe=reclass");
|
||||
|
||||
// ── Debugger Tools status ──
|
||||
layout->addSpacing(8);
|
||||
#ifdef _WIN32
|
||||
bool found = dbgToolsFound();
|
||||
auto* toolsRow = new QHBoxLayout;
|
||||
auto* toolsLabel = new QLabel;
|
||||
if (found) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
} else {
|
||||
toolsLabel->setText("Debugging Tools: not found");
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, QColor(220, 120, 80));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
toolsLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
toolsRow->addWidget(toolsLabel, 1);
|
||||
|
||||
auto* browseBtn = new QPushButton("Browse...");
|
||||
browseBtn->setFixedWidth(70);
|
||||
browseBtn->setToolTip("Locate Debugging Tools for Windows directory (contains dbgeng.dll)");
|
||||
QObject::connect(browseBtn, &QPushButton::clicked, [&dlg, toolsLabel, &dlgPal]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(&dlg,
|
||||
"Locate Debugging Tools for Windows",
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers");
|
||||
if (dir.isEmpty()) return;
|
||||
QString dllPath = dir + "/dbgeng.dll";
|
||||
if (!QFileInfo::exists(dllPath)) {
|
||||
QMessageBox::warning(&dlg, "Not Found",
|
||||
"dbgeng.dll was not found in that directory.\n"
|
||||
"Select the folder containing dbgeng.dll\n"
|
||||
"(e.g. Debuggers\\x64).");
|
||||
return;
|
||||
}
|
||||
QSettings settings;
|
||||
settings.setValue(kSettingsKey, dir);
|
||||
// Force reload on next use
|
||||
s_hDbgEng = nullptr;
|
||||
s_loadedDir.clear();
|
||||
if (dbgToolsFound()) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
});
|
||||
toolsRow->addWidget(browseBtn);
|
||||
layout->addLayout(toolsRow);
|
||||
|
||||
if (!found) {
|
||||
auto* note = new QLabel(
|
||||
"The system dbgeng.dll does not support remote connections.\n"
|
||||
"Install Debugging Tools for Windows or use Browse to locate them.");
|
||||
QPalette np = dlgPal;
|
||||
np.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
note->setPalette(np);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
}
|
||||
#endif
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
auto* btnLayout = new QHBoxLayout;
|
||||
|
||||
105
src/compose.cpp
105
src/compose.cpp
@@ -53,7 +53,7 @@ struct ComposeState {
|
||||
siblingStack[d] = hasMoreSiblings;
|
||||
}
|
||||
|
||||
void emitLine(const QString& lineText, LineMeta lm) {
|
||||
void emitLine(const QString& lineText, LineMeta&& lm) {
|
||||
if (currentLine > 0) text += '\n';
|
||||
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
|
||||
// CommandRow has no fold prefix (flush left)
|
||||
@@ -87,7 +87,7 @@ struct ComposeState {
|
||||
text += lineText;
|
||||
}
|
||||
|
||||
meta.append(lm);
|
||||
meta.append(std::move(lm));
|
||||
currentLine++;
|
||||
}
|
||||
};
|
||||
@@ -208,7 +208,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
state.emitLine(lineText, lm);
|
||||
state.emitLine(lineText, std::move(lm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
|
||||
node.name + QStringLiteral(" */"), lm);
|
||||
node.name + QStringLiteral(" */"), std::move(lm));
|
||||
return;
|
||||
}
|
||||
state.visiting.insert(node.id);
|
||||
@@ -267,7 +267,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.arrayElementIdx = arrayElementIdx;
|
||||
uint64_t relOff = absAddr - arrayContainerAddr;
|
||||
QString relOffHex = QString::number(relOff, 16).toUpper();
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), lm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), std::move(lm));
|
||||
}
|
||||
|
||||
// Detect root header: first root-level struct — suppressed from display
|
||||
@@ -325,7 +325,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
headerText.chop(1);
|
||||
// Remove trailing separator spaces
|
||||
while (headerText.endsWith(' ')) headerText.chop(1);
|
||||
state.emitLine(headerText, lm);
|
||||
state.emitLine(headerText, std::move(lm));
|
||||
// Emit standalone brace line
|
||||
LineMeta braceLm;
|
||||
braceLm.nodeIdx = nodeIdx;
|
||||
@@ -334,9 +334,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||
braceLm.markerMask = (1u << M_STRUCT_BG);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
||||
} else {
|
||||
state.emitLine(headerText, lm);
|
||||
state.emitLine(headerText, std::move(lm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
|
||||
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), std::move(lm));
|
||||
}
|
||||
|
||||
// Footer
|
||||
@@ -389,7 +389,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -423,7 +423,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
|
||||
childDepth, maxNameLen), lm);
|
||||
childDepth, maxNameLen), std::move(lm));
|
||||
}
|
||||
|
||||
// Footer
|
||||
@@ -441,7 +441,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -501,7 +501,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
||||
{}, eTW, eNW, elemTypeStr,
|
||||
state.compactColumns), lm);
|
||||
state.compactColumns), std::move(lm));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm));
|
||||
continue;
|
||||
}
|
||||
composeNode(state, tree, prov, childIdx, childDepth,
|
||||
@@ -587,7 +587,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
|
||||
// ── Static fields: render after regular children, before footer ──
|
||||
if (!staticIdxs.isEmpty() && !node.collapsed) {
|
||||
if (!staticIdxs.isEmpty() && (!node.collapsed || isRootHeader)) {
|
||||
// Build identifier resolver for static field expressions
|
||||
auto makeResolver = [&](uint64_t parentAbsAddr) {
|
||||
AddressParserCallbacks cbs;
|
||||
@@ -700,7 +700,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
|
||||
lm.effectiveNameW = sf.name.size();
|
||||
state.emitLine(headerLine, lm);
|
||||
state.emitLine(headerLine, std::move(lm));
|
||||
|
||||
// ── Body + children (only when expanded) ──
|
||||
if (!isCollapsed) {
|
||||
@@ -747,7 +747,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
||||
blm.offsetAddr = staticAddr;
|
||||
blm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(bodyLine, blm);
|
||||
state.emitLine(bodyLine, std::move(blm));
|
||||
}
|
||||
|
||||
// If struct/array, compose children at evaluated address
|
||||
@@ -780,7 +780,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
flm.offsetAddr = staticAddr;
|
||||
}
|
||||
flm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
|
||||
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), std::move(flm));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -802,7 +802,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
@@ -865,7 +865,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
|
||||
ptrText.chop(1);
|
||||
while (ptrText.endsWith(' ')) ptrText.chop(1);
|
||||
state.emitLine(ptrText, lm);
|
||||
state.emitLine(ptrText, std::move(lm));
|
||||
LineMeta braceLm;
|
||||
braceLm.nodeIdx = nodeIdx;
|
||||
braceLm.nodeId = node.id;
|
||||
@@ -873,9 +873,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = computeFoldLevel(depth, true);
|
||||
braceLm.markerMask = lm.markerMask;
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
||||
} else {
|
||||
state.emitLine(ptrText, lm);
|
||||
state.emitLine(ptrText, std::move(lm));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -955,7 +955,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.offsetText.clear();
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm);
|
||||
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -988,10 +988,32 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
});
|
||||
}
|
||||
|
||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||
// Pre-allocate output buffers (estimate ~3 lines per node, ~80 chars per line)
|
||||
state.meta.reserve(tree.nodes.size() * 3);
|
||||
state.text.reserve(tree.nodes.size() * 80);
|
||||
|
||||
// Precompute absolute offsets via BFS (O(N) — avoids per-node parent-chain walk)
|
||||
state.absOffsets.resize(tree.nodes.size());
|
||||
state.absOffsets.fill(0);
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
|
||||
if (tree.nodes[i].parentId == 0)
|
||||
state.absOffsets[i] = tree.nodes[i].offset;
|
||||
{
|
||||
QVector<int> bfsQueue;
|
||||
for (int i : state.childMap.value(0))
|
||||
bfsQueue.append(i);
|
||||
int front = 0;
|
||||
while (front < bfsQueue.size()) {
|
||||
int idx = bfsQueue[front++];
|
||||
int pi = tree.indexOfId(tree.nodes[idx].parentId);
|
||||
state.absOffsets[idx] = (pi >= 0 ? state.absOffsets[pi] : 0)
|
||||
+ tree.nodes[idx].offset;
|
||||
for (int ci : state.childMap.value(tree.nodes[idx].id))
|
||||
bfsQueue.append(ci);
|
||||
}
|
||||
}
|
||||
for (auto& v : state.absOffsets)
|
||||
v += tree.baseAddress;
|
||||
|
||||
// Compute hex digit tier from max absolute address
|
||||
{
|
||||
@@ -1020,23 +1042,21 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
return fmt::typeNameRaw(n.kind);
|
||||
};
|
||||
|
||||
// Compute effective type column width from longest type name
|
||||
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
||||
// Pre-compute type name lengths (avoids re-creating temp QStrings in width loops)
|
||||
QVector<int> typeNameLens(tree.nodes.size());
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
typeNameLens[i] = nodeTypeName(tree.nodes[i]).size();
|
||||
|
||||
// Compute effective column widths from longest type/name in a single pass
|
||||
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
|
||||
int maxTypeLen = kMinTypeW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
||||
int maxNameLen = kMinNameW;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
maxTypeLen = qMax(maxTypeLen, typeNameLens[i]);
|
||||
if (!isHexPreview(tree.nodes[i].kind))
|
||||
maxNameLen = qMax(maxNameLen, (int)tree.nodes[i].name.size());
|
||||
}
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
|
||||
|
||||
// Compute effective name column width from longest name
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip hex (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||
|
||||
// Pre-compute per-scope widths (each container gets widths based on direct children only)
|
||||
@@ -1053,7 +1073,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
||||
if (child.kind == NodeKind::Struct)
|
||||
continue;
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]);
|
||||
|
||||
// Name width (skip hex, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
@@ -1079,7 +1099,6 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
}
|
||||
|
||||
// Compute scope widths for root level (parentId == 0)
|
||||
// Include struct/array headers - they now use columnar layout too
|
||||
{
|
||||
int rootMaxType = kMinTypeW;
|
||||
int rootMaxName = kMinNameW;
|
||||
@@ -1088,7 +1107,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
||||
if (child.kind == NodeKind::Struct)
|
||||
continue;
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]);
|
||||
|
||||
// Name width (skip hex, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
@@ -1115,7 +1134,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
state.emitLine(cmdRowText, lm);
|
||||
state.emitLine(cmdRowText, std::move(lm));
|
||||
}
|
||||
|
||||
// Brace wrapping: emit standalone "{" after CommandRow
|
||||
@@ -1127,7 +1146,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
braceLm.lineKind = LineKind::Header;
|
||||
braceLm.foldLevel = SC_FOLDLEVELBASE;
|
||||
braceLm.markerMask = 0;
|
||||
state.emitLine(QStringLiteral("{"), braceLm);
|
||||
state.emitLine(QStringLiteral("{"), std::move(braceLm));
|
||||
}
|
||||
|
||||
const QVector<int>& roots = childIndices(state, 0);
|
||||
|
||||
@@ -231,17 +231,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
TypePopupMode mode = TypePopupMode::FieldType;
|
||||
if (target == EditTarget::ArrayElementType)
|
||||
mode = TypePopupMode::ArrayElement;
|
||||
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;
|
||||
}
|
||||
// PointerTarget is handled as FieldType — modifiers * / ** will be pre-selected
|
||||
showTypePopup(editor, mode, nodeIdx, globalPos);
|
||||
});
|
||||
|
||||
@@ -1787,7 +1777,41 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
&& !node.bitfieldMembers.isEmpty()
|
||||
&& subLine >= 0 && subLine < node.bitfieldMembers.size();
|
||||
|
||||
bool isEnumNode = node.resolvedClassKeyword() == QStringLiteral("enum");
|
||||
|
||||
if (isEnumMember || isBitfieldMember) {
|
||||
if (isEnumMember) {
|
||||
menu.addAction(icon("diff-added.svg"), "Add Member Above", [this, nodeId, subLine]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
auto members = m_doc->tree.nodes[ni].enumMembers;
|
||||
int64_t val = (subLine > 0) ? members[subLine - 1].second + 1 : 0;
|
||||
auto oldMembers = members;
|
||||
members.insert(subLine, {QStringLiteral("NewMember"), val});
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
|
||||
});
|
||||
menu.addAction(icon("diff-added.svg"), "Add Member Below", [this, nodeId, subLine]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
auto members = m_doc->tree.nodes[ni].enumMembers;
|
||||
int64_t val = members[subLine].second + 1;
|
||||
auto oldMembers = members;
|
||||
members.insert(subLine + 1, {QStringLiteral("NewMember"), val});
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
|
||||
});
|
||||
menu.addAction(icon("trash.svg"), "Remove Member", [this, nodeId, subLine]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
auto members = m_doc->tree.nodes[ni].enumMembers;
|
||||
auto oldMembers = members;
|
||||
members.remove(subLine);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
|
||||
});
|
||||
menu.addSeparator();
|
||||
}
|
||||
if (isBitfieldMember) {
|
||||
const auto& bm = node.bitfieldMembers[subLine];
|
||||
if (bm.bitWidth == 1) {
|
||||
@@ -1802,6 +1826,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
menu.addSeparator();
|
||||
}
|
||||
// Fall through to always-available actions
|
||||
} else if (isEnumNode) {
|
||||
// Enum header line — enum-specific actions only (no struct ops)
|
||||
menu.addAction(icon("diff-added.svg"), "Add Member", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
auto members = m_doc->tree.nodes[ni].enumMembers;
|
||||
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
|
||||
auto oldMembers = members;
|
||||
members.append({QStringLiteral("NewMember"), nextVal});
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeEnumMembers{nodeId, oldMembers, members}));
|
||||
});
|
||||
menu.addAction(icon("edit.svg"), "&Rename...", [this, editor, line]() {
|
||||
editor->beginInlineEdit(EditTarget::Name, line);
|
||||
});
|
||||
menu.addSeparator();
|
||||
menu.addAction(icon("trash.svg"), "&Delete", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) removeNode(ni);
|
||||
});
|
||||
menu.addSeparator();
|
||||
// Fall through to always-available actions
|
||||
} else {
|
||||
|
||||
// ── Quick-convert suggestions (top-level for fast access) ──
|
||||
@@ -2775,7 +2821,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
}
|
||||
|
||||
// Add types from other open documents
|
||||
if (mode != TypePopupMode::Root && m_projectDocs) {
|
||||
if (m_projectDocs) {
|
||||
QSet<QString> localNames;
|
||||
for (const auto& e : entries)
|
||||
if (e.entryKind == TypeEntry::Composite)
|
||||
|
||||
19
src/core.h
19
src/core.h
@@ -199,7 +199,7 @@ struct Node {
|
||||
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
|
||||
int arrayLen = 1; // Array: element count
|
||||
int strLen = 64;
|
||||
bool collapsed = false;
|
||||
bool collapsed = true;
|
||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||
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**
|
||||
@@ -285,7 +285,7 @@ struct Node {
|
||||
n.offsetExpr = o["offsetExpr"].toString();
|
||||
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
|
||||
n.collapsed = o["collapsed"].toBool(false);
|
||||
n.collapsed = o["collapsed"].toBool(true);
|
||||
n.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||
@@ -333,6 +333,7 @@ struct NodeTree {
|
||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
mutable QHash<uint64_t, QVector<int>> m_childCache;
|
||||
|
||||
int addNode(const Node& n) {
|
||||
Node copy = n;
|
||||
@@ -342,13 +343,15 @@ struct NodeTree {
|
||||
nodes.append(copy);
|
||||
if (!m_idCache.isEmpty())
|
||||
m_idCache[copy.id] = idx;
|
||||
if (!m_childCache.isEmpty())
|
||||
m_childCache[copy.parentId].append(idx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Reserve a unique ID atomically (for use before pushing undo commands)
|
||||
uint64_t reserveId() { return m_nextId++; }
|
||||
|
||||
void invalidateIdCache() const { m_idCache.clear(); }
|
||||
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
|
||||
|
||||
int indexOfId(uint64_t id) const {
|
||||
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
|
||||
@@ -359,11 +362,11 @@ struct NodeTree {
|
||||
}
|
||||
|
||||
QVector<int> childrenOf(uint64_t parentId) const {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i].parentId == parentId) result.append(i);
|
||||
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++)
|
||||
m_childCache[nodes[i].parentId].append(i);
|
||||
}
|
||||
return result;
|
||||
return m_childCache.value(parentId);
|
||||
}
|
||||
|
||||
// Collect node + all descendants (iterative, cycle-safe)
|
||||
@@ -483,6 +486,7 @@ struct NodeTree {
|
||||
t.pointerSize = o["pointerSize"].toInt(8);
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
t.nodes.reserve(arr.size());
|
||||
for (const auto& v : arr) {
|
||||
Node n = Node::fromJson(v.toObject());
|
||||
t.nodes.append(n);
|
||||
@@ -571,6 +575,7 @@ static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits
|
||||
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||
Q_ASSERT(elemIdx >= 0);
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
||||
}
|
||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
|
||||
@@ -5,28 +5,18 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
|
||||
// Dock tab button widget (pin + close)
|
||||
// Dock tab button widget (close button)
|
||||
// Placed on the right side of each dock tab via QTabBar::setTabButton.
|
||||
class DockTabButtons : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
QToolButton* pinBtn;
|
||||
QToolButton* closeBtn;
|
||||
bool pinned = false;
|
||||
|
||||
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* hl = new QHBoxLayout(this);
|
||||
hl->setContentsMargins(0, 0, 0, 0);
|
||||
hl->setSpacing(0);
|
||||
|
||||
pinBtn = new QToolButton(this);
|
||||
pinBtn->setAutoRaise(true);
|
||||
pinBtn->setCursor(Qt::PointingHandCursor);
|
||||
pinBtn->setFixedSize(16, 16);
|
||||
pinBtn->setToolTip("Pin tab");
|
||||
updatePinIcon();
|
||||
hl->addWidget(pinBtn);
|
||||
|
||||
closeBtn = new QToolButton(this);
|
||||
closeBtn->setAutoRaise(true);
|
||||
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||
@@ -35,31 +25,12 @@ public:
|
||||
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
|
||||
closeBtn->setIconSize(QSize(12, 12));
|
||||
hl->addWidget(closeBtn);
|
||||
|
||||
connect(pinBtn, &QToolButton::clicked, this, [this]() {
|
||||
pinned = !pinned;
|
||||
updatePinIcon();
|
||||
emit pinToggled(pinned);
|
||||
});
|
||||
}
|
||||
|
||||
void applyTheme(const QColor& hover) {
|
||||
QString style = QStringLiteral(
|
||||
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||
pinBtn->setStyleSheet(style);
|
||||
closeBtn->setStyleSheet(style);
|
||||
}
|
||||
|
||||
void setPinned(bool p) { pinned = p; updatePinIcon(); emit pinToggled(pinned); }
|
||||
|
||||
signals:
|
||||
void pinToggled(bool pinned);
|
||||
|
||||
private:
|
||||
void updatePinIcon() {
|
||||
pinBtn->setIcon(QIcon(pinned ? ":/vsicons/pinned.svg" : ":/vsicons/pin.svg"));
|
||||
pinBtn->setIconSize(QSize(12, 12));
|
||||
pinBtn->setToolTip(pinned ? "Unpin tab" : "Pin tab");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -562,9 +562,11 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
if (chosen == actRel && !m_relativeOffsets) {
|
||||
m_relativeOffsets = true;
|
||||
reformatMargins();
|
||||
emit relativeOffsetsChanged(true);
|
||||
} else if (chosen == actAbs && m_relativeOffsets) {
|
||||
m_relativeOffsets = false;
|
||||
reformatMargins();
|
||||
emit relativeOffsetsChanged(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -837,8 +839,11 @@ void RcxEditor::allocateMarginStyles() {
|
||||
}
|
||||
|
||||
void RcxEditor::applyTheme(const Theme& theme) {
|
||||
// Editor uses a slightly darker background than chrome for visual depth
|
||||
const QColor editorBg = theme.background.darker(115);
|
||||
|
||||
// Paper and text
|
||||
m_sci->setPaper(theme.background);
|
||||
m_sci->setPaper(editorBg);
|
||||
m_sci->setColor(theme.text);
|
||||
m_sci->setCaretForegroundColor(theme.text);
|
||||
|
||||
@@ -882,25 +887,25 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
m_lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
||||
m_lexer->setColor(theme.syntaxType, QsciLexerCPP::GlobalClass);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
m_lexer->setPaper(theme.background, i);
|
||||
m_lexer->setPaper(editorBg, i);
|
||||
|
||||
// Margins
|
||||
m_sci->setMarginsBackgroundColor(theme.background);
|
||||
m_sci->setMarginsBackgroundColor(editorBg);
|
||||
m_sci->setMarginsForegroundColor(theme.textFaint);
|
||||
m_sci->setFoldMarginColors(theme.background, theme.background);
|
||||
m_sci->setFoldMarginColors(editorBg, editorBg);
|
||||
|
||||
// Markers
|
||||
m_sci->setMarkerBackgroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerForegroundColor(theme.markerPtr, M_PTR0);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(theme.background, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(editorBg, M_CYCLE);
|
||||
m_sci->setMarkerForegroundColor(editorBg, M_CYCLE);
|
||||
m_sci->setMarkerBackgroundColor(theme.markerError, M_ERR);
|
||||
m_sci->setMarkerForegroundColor(theme.text, M_ERR);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_STRUCT_BG);
|
||||
m_sci->setMarkerBackgroundColor(editorBg, M_STRUCT_BG);
|
||||
m_sci->setMarkerForegroundColor(theme.text, M_STRUCT_BG);
|
||||
m_sci->setMarkerBackgroundColor(theme.hover, M_HOVER);
|
||||
m_sci->setMarkerBackgroundColor(theme.selected, M_SELECTED);
|
||||
m_sci->setMarkerBackgroundColor(theme.background, M_CMD_ROW);
|
||||
m_sci->setMarkerBackgroundColor(editorBg, M_CMD_ROW);
|
||||
m_sci->setMarkerBackgroundColor(theme.indHoverSpan, M_ACCENT);
|
||||
|
||||
// Margin extended styles
|
||||
@@ -911,7 +916,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
|
||||
abs, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
|
||||
abs, theme.background);
|
||||
abs, editorBg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,8 +960,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
}
|
||||
|
||||
// Dynamically resize margin to fit the current hex digit tier
|
||||
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
// RVA mode uses half width since relative offsets are much shorter
|
||||
{
|
||||
int marginDigits = m_relativeOffsets
|
||||
? qMax(m_layout.offsetHexDigits / 2, 4)
|
||||
: m_layout.offsetHexDigits;
|
||||
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
}
|
||||
|
||||
m_sci->setReadOnly(false);
|
||||
m_sci->setText(result.text);
|
||||
@@ -1063,6 +1074,11 @@ void RcxEditor::reformatMargins() {
|
||||
uint64_t base = m_layout.baseAddress;
|
||||
int hexDigits = m_layout.offsetHexDigits;
|
||||
|
||||
// Resize margin: RVA offsets are much shorter than full addresses
|
||||
int marginDigits = m_relativeOffsets ? qMax(hexDigits / 2, 4) : hexDigits;
|
||||
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
|
||||
// ── Pass 1: margin text (global offset only) ──
|
||||
m_sci->clearMarginText(-1);
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
@@ -2192,6 +2208,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
#endif
|
||||
m_relativeOffsets = !m_relativeOffsets;
|
||||
reformatMargins();
|
||||
emit relativeOffsetsChanged(m_relativeOffsets);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2271,7 +2288,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
|
||||
}
|
||||
// Resolve hovered nodeId on move/wheel (non-edit mode only)
|
||||
if (!m_editState.active &&
|
||||
// Guard: skip during applyDocument — m_nodeLineIndex may be stale
|
||||
if (!m_editState.active && !m_applyingDocument &&
|
||||
(event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) {
|
||||
auto h = hitTest(m_lastHoverPos);
|
||||
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
|
||||
@@ -3599,8 +3617,13 @@ void RcxEditor::setEditorFont(const QString& fontName) {
|
||||
// Re-apply margin styles and width with new font metrics
|
||||
allocateMarginStyles();
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
{
|
||||
int marginDigits = m_relativeOffsets
|
||||
? qMax(m_layout.offsetHexDigits / 2, 4)
|
||||
: m_layout.offsetHexDigits;
|
||||
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::setGlobalFontName(const QString& fontName) {
|
||||
|
||||
@@ -84,6 +84,7 @@ signals:
|
||||
void typeSelectorRequested();
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
void relativeOffsetsChanged(bool relative);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
171842
src/examples/WinSDK.rcx
171842
src/examples/WinSDK.rcx
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,7 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
|
||||
// ── Value formatting ──
|
||||
|
||||
static QString hexVal(uint64_t v) {
|
||||
return QStringLiteral("0x") + QString::number(v, 16);
|
||||
return QString::asprintf("0x%llx", (unsigned long long)v);
|
||||
}
|
||||
|
||||
static QString rawHex(uint64_t v, int digits) {
|
||||
@@ -228,15 +228,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static const char kHexDigits[] = "0123456789ABCDEF";
|
||||
|
||||
static QString bytesToHex(const QByteArray& b, int slot) {
|
||||
QString out;
|
||||
out.reserve(slot * 3);
|
||||
QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
|
||||
int pos = 0;
|
||||
for (int i = 0; i < slot; ++i) {
|
||||
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
|
||||
out += QString::asprintf("%02X", (unsigned)c);
|
||||
if (i + 1 < slot) out += ' ';
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
|
||||
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
|
||||
}
|
||||
return out;
|
||||
return QString(buf, pos);
|
||||
}
|
||||
|
||||
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
|
||||
@@ -715,6 +718,7 @@ uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
||||
default: container = prov.readU64(addr); break;
|
||||
}
|
||||
Q_ASSERT(bitOffset + bitWidth <= 64);
|
||||
if (bitWidth >= 64) return container >> bitOffset;
|
||||
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
||||
}
|
||||
|
||||
@@ -332,7 +332,10 @@ struct Parser {
|
||||
QVector<ParsedStruct> structs;
|
||||
QSet<QString> forwardDecls;
|
||||
QHash<QString, QString> typedefs; // alias -> real type
|
||||
QSet<QString> pointerTypedefs; // aliases that are pointer-to-struct
|
||||
QHash<QString, QVector<int>> arrayTypedefs; // aliases that are array types (alias -> dimensions)
|
||||
QHash<QString, int> sizeAsserts; // struct name -> declared size
|
||||
QHash<QString, int> structAlignments; // struct name -> ALIGN(N) value
|
||||
|
||||
explicit Parser(const QVector<Token>& t, const QVector<LineOffset>& lo)
|
||||
: tokens(t), lineOffsets(lo) {}
|
||||
@@ -375,12 +378,57 @@ struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip ALIGN( N ) macro if present (Vergilius-style headers)
|
||||
// Returns the alignment value, or 0 if no ALIGN macro.
|
||||
int skipAlignMacro() {
|
||||
if (checkIdent("ALIGN") || checkIdent("__declspec")) {
|
||||
advance();
|
||||
int alignVal = 0;
|
||||
if (match(TokKind::LParen)) {
|
||||
// Try to read the alignment number
|
||||
if (peek().kind == TokKind::Number) {
|
||||
alignVal = peek().text.toInt();
|
||||
}
|
||||
int depth = 1;
|
||||
while (depth > 0 && peek().kind != TokKind::Eof) {
|
||||
if (peek().kind == TokKind::LParen) depth++;
|
||||
else if (peek().kind == TokKind::RParen) depth--;
|
||||
advance();
|
||||
}
|
||||
}
|
||||
return alignVal;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if next tokens after keyword are ALIGN(...) then Ident/LBrace
|
||||
bool peekPastAlign(int offset, TokKind expected) const {
|
||||
int i = cur + offset;
|
||||
if (i < tokens.size() && tokens[i].kind == TokKind::Ident &&
|
||||
(tokens[i].text == QStringLiteral("ALIGN") ||
|
||||
tokens[i].text == QStringLiteral("__declspec"))) {
|
||||
i++; // skip ALIGN
|
||||
if (i < tokens.size() && tokens[i].kind == TokKind::LParen) {
|
||||
int depth = 1; i++;
|
||||
while (i < tokens.size() && depth > 0) {
|
||||
if (tokens[i].kind == TokKind::LParen) depth++;
|
||||
else if (tokens[i].kind == TokKind::RParen) depth--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return i < tokens.size() && tokens[i].kind == expected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Top-level parse ──
|
||||
|
||||
void parse() {
|
||||
while (peek().kind != TokKind::Eof) {
|
||||
if (checkIdent("struct") || checkIdent("class")) {
|
||||
parseStructOrForward();
|
||||
} else if (checkIdent("union")) {
|
||||
parseTopLevelUnion();
|
||||
} else if (checkIdent("static_assert")) {
|
||||
parseStaticAssert();
|
||||
} else if (checkIdent("typedef")) {
|
||||
@@ -400,6 +448,9 @@ struct Parser {
|
||||
void parseStructOrForward() {
|
||||
QString keyword = advance().text; // "struct" or "class"
|
||||
|
||||
// Skip ALIGN( N ) between keyword and name
|
||||
int alignVal = skipAlignMacro();
|
||||
|
||||
// Anonymous struct: struct { ... }
|
||||
if (check(TokKind::LBrace)) {
|
||||
// Skip anonymous struct at top level
|
||||
@@ -411,6 +462,9 @@ struct Parser {
|
||||
if (!check(TokKind::Ident)) { skipToSemiOrBrace(); return; }
|
||||
QString name = advance().text;
|
||||
|
||||
if (alignVal > 0)
|
||||
structAlignments[name] = alignVal;
|
||||
|
||||
// Check for inheritance: struct Foo : public Bar {
|
||||
// Just skip the inheritance clause
|
||||
if (check(TokKind::Colon)) {
|
||||
@@ -446,14 +500,18 @@ struct Parser {
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
// Nested struct definition
|
||||
if (checkIdent("struct") || checkIdent("class")) {
|
||||
if (peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
// Check: struct [ALIGN(N)] Name {
|
||||
if ((peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) ||
|
||||
peekPastAlign(1, TokKind::Ident)) {
|
||||
// Nested named struct: parse as a top-level struct, then treat as embedded field
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
if (peek(1).kind == TokKind::LBrace) {
|
||||
// Check: struct [ALIGN(N)] {
|
||||
if (peek(1).kind == TokKind::LBrace || peekPastAlign(1, TokKind::LBrace)) {
|
||||
// Anonymous nested struct { ... } fieldName;
|
||||
advance(); // skip "struct"
|
||||
skipAlignMacro();
|
||||
advance(); // skip "{"
|
||||
// Skip body
|
||||
int depth = 1;
|
||||
@@ -499,9 +557,54 @@ struct Parser {
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level named union definition: union [ALIGN(N)] Name { ... };
|
||||
// Parsed as a struct with classKeyword "union" and all members as fields
|
||||
void parseTopLevelUnion() {
|
||||
advance(); // skip "union"
|
||||
int alignVal = skipAlignMacro();
|
||||
|
||||
// Forward declaration: union Name;
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::Semi) {
|
||||
QString name = advance().text;
|
||||
advance(); // skip ;
|
||||
forwardDecls.insert(name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Anonymous union at top level (skip)
|
||||
if (check(TokKind::LBrace)) {
|
||||
skipToSemiOrBrace();
|
||||
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (!check(TokKind::Ident)) { skipToSemiOrBrace(); return; }
|
||||
QString name = advance().text;
|
||||
|
||||
if (alignVal > 0)
|
||||
structAlignments[name] = alignVal;
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
ParsedStruct ps;
|
||||
ps.name = name;
|
||||
ps.keyword = QStringLiteral("union");
|
||||
|
||||
// Parse body — same as struct body but members overlap at offset 0
|
||||
parseStructBody(ps);
|
||||
|
||||
if (!match(TokKind::RBrace)) { skipToSemiOrBrace(); return; }
|
||||
match(TokKind::Semi);
|
||||
|
||||
structs.append(ps);
|
||||
}
|
||||
|
||||
void parseUnion(ParsedStruct& ps) {
|
||||
advance(); // skip "union"
|
||||
|
||||
// Skip ALIGN( N ) between union keyword and name/brace
|
||||
skipAlignMacro();
|
||||
|
||||
// Optional union tag name (before {)
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip union tag name
|
||||
@@ -525,9 +628,11 @@ struct Parser {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle anonymous struct inside union: struct { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
|
||||
// Handle anonymous struct inside union: struct [ALIGN(N)] { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
(peek(1).kind == TokKind::LBrace || peekPastAlign(1, TokKind::LBrace))) {
|
||||
advance(); // skip "struct"
|
||||
skipAlignMacro();
|
||||
advance(); // skip "{"
|
||||
int depth = 1;
|
||||
while (peek().kind != TokKind::Eof && depth > 0) {
|
||||
@@ -541,9 +646,10 @@ struct Parser {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested named struct definition inside union
|
||||
// Handle nested named struct definition inside union: struct [ALIGN(N)] Name {
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
((peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) ||
|
||||
peekPastAlign(1, TokKind::Ident))) {
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
@@ -584,13 +690,26 @@ struct Parser {
|
||||
QString typeName = parseTypeName();
|
||||
if (typeName.isEmpty()) { cur = startPos; return false; }
|
||||
|
||||
// Resolve typedef
|
||||
while (typedefs.contains(typeName))
|
||||
typeName = typedefs[typeName];
|
||||
// Resolve typedef — track pointer and array typedefs in the chain
|
||||
bool typedefPointer = false;
|
||||
QVector<int> typedefArrayDims;
|
||||
{
|
||||
QString resolved = typeName;
|
||||
QSet<QString> seen;
|
||||
while (typedefs.contains(resolved) && !seen.contains(resolved)) {
|
||||
if (pointerTypedefs.contains(resolved))
|
||||
typedefPointer = true;
|
||||
if (typedefArrayDims.isEmpty() && arrayTypedefs.contains(resolved))
|
||||
typedefArrayDims = arrayTypedefs[resolved];
|
||||
seen.insert(resolved);
|
||||
resolved = typedefs[resolved];
|
||||
}
|
||||
typeName = resolved;
|
||||
}
|
||||
|
||||
// Pointer stars
|
||||
bool isPointer = false;
|
||||
int ptrDepth = 0;
|
||||
bool isPointer = typedefPointer;
|
||||
int ptrDepth = typedefPointer ? 1 : 0;
|
||||
while (match(TokKind::Star)) {
|
||||
isPointer = true;
|
||||
ptrDepth++;
|
||||
@@ -628,6 +747,18 @@ struct Parser {
|
||||
match(TokKind::RBracket);
|
||||
}
|
||||
|
||||
// Apply array dimensions from typedef (e.g. typedef ULONG GDI_HANDLE_BUFFER[60])
|
||||
if (!typedefArrayDims.isEmpty()) {
|
||||
if (field.arraySizes.isEmpty())
|
||||
field.arraySizes = typedefArrayDims;
|
||||
else {
|
||||
// Combine: typedef dims come first, field dims appended
|
||||
QVector<int> combined = typedefArrayDims;
|
||||
combined.append(field.arraySizes);
|
||||
field.arraySizes = combined;
|
||||
}
|
||||
}
|
||||
|
||||
// Bitfield: Type name : width
|
||||
if (check(TokKind::Colon)) {
|
||||
advance();
|
||||
@@ -750,28 +881,83 @@ struct Parser {
|
||||
parseStructOrForward();
|
||||
return;
|
||||
}
|
||||
// typedef struct ExistingName AliasName;
|
||||
// typedef struct ExistingName * AliasName;
|
||||
advance(); // skip struct/class
|
||||
if (check(TokKind::Ident)) {
|
||||
QString existingName = advance().text;
|
||||
// Pointer stars
|
||||
while (match(TokKind::Star)) {}
|
||||
bool hasPtr = false;
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
// Skip const/volatile after pointer
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
if (check(TokKind::Ident)) {
|
||||
QString aliasName = advance().text;
|
||||
typedefs[aliasName] = existingName;
|
||||
if (aliasName != existingName) { // skip self-referencing typedefs
|
||||
typedefs[aliasName] = existingName;
|
||||
if (hasPtr) pointerTypedefs.insert(aliasName);
|
||||
}
|
||||
}
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
return;
|
||||
}
|
||||
|
||||
// typedef BaseType AliasName;
|
||||
// typedef BaseType [*] AliasName [N];
|
||||
// Skip leading const/volatile qualifiers: typedef const Type* Alias;
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
QString baseType = parseTypeName();
|
||||
if (baseType.isEmpty()) { skipToSemiOrBrace(); return; }
|
||||
while (match(TokKind::Star)) {} // pointer typedefs
|
||||
bool hasPtr = false;
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
// Skip const/volatile after pointer
|
||||
while (checkIdent("const") || checkIdent("volatile")) advance();
|
||||
while (match(TokKind::Star)) { hasPtr = true; }
|
||||
|
||||
// Function pointer typedef: typedef RetType ( *Name )( args... );
|
||||
if (check(TokKind::LParen)) {
|
||||
int save = cur;
|
||||
advance(); // skip (
|
||||
bool isFnPtr = false;
|
||||
QString fnName;
|
||||
if (match(TokKind::Star) && check(TokKind::Ident)) {
|
||||
fnName = advance().text;
|
||||
if (match(TokKind::RParen) && check(TokKind::LParen)) {
|
||||
isFnPtr = true;
|
||||
}
|
||||
}
|
||||
if (isFnPtr) {
|
||||
// Skip the argument list and register as pointer type
|
||||
skipToSemiOrBrace();
|
||||
pointerTypedefs.insert(fnName);
|
||||
typedefs[fnName] = QStringLiteral("void");
|
||||
} else {
|
||||
cur = save;
|
||||
skipToSemiOrBrace();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (check(TokKind::Ident)) {
|
||||
QString alias = advance().text;
|
||||
typedefs[alias] = baseType;
|
||||
// Array dimensions: typedef Type Name[N][M];
|
||||
QVector<int> dims;
|
||||
while (check(TokKind::LBracket)) {
|
||||
advance();
|
||||
if (check(TokKind::Number)) {
|
||||
bool ok;
|
||||
QString numText = peek().text;
|
||||
int val = numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)
|
||||
? numText.mid(2).toInt(&ok, 16) : numText.toInt(&ok);
|
||||
if (ok) dims.append(val);
|
||||
advance();
|
||||
}
|
||||
match(TokKind::RBracket);
|
||||
}
|
||||
if (alias != baseType) { // skip self-referencing typedefs
|
||||
typedefs[alias] = baseType;
|
||||
if (hasPtr) pointerTypedefs.insert(alias);
|
||||
if (!dims.isEmpty()) arrayTypedefs[alias] = dims;
|
||||
}
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
}
|
||||
@@ -945,8 +1131,62 @@ struct BuildContext {
|
||||
bool useCommentOffsets;
|
||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||
int ptrSize = 8; // target pointer size (4 or 8)
|
||||
const QHash<QString, int>& sizeAsserts; // declared struct sizes from static_assert
|
||||
const QHash<QString, int>& structAlignments; // struct name -> ALIGN(N) value
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
static int fieldNaturalAlignment(const ParsedField& field, const BuildContext& ctx);
|
||||
|
||||
// Compute natural alignment for a union from its members (max member alignment)
|
||||
static int unionNaturalAlignment(const ParsedField& field, const BuildContext& ctx) {
|
||||
int maxAlign = 1;
|
||||
for (const auto& member : field.unionMembers) {
|
||||
int a = fieldNaturalAlignment(member, ctx);
|
||||
if (a > maxAlign) maxAlign = a;
|
||||
}
|
||||
return maxAlign;
|
||||
}
|
||||
|
||||
// Return natural alignment for a parsed field (used when computing offsets without comments)
|
||||
static int fieldNaturalAlignment(const ParsedField& field, const BuildContext& ctx) {
|
||||
if (field.isPointer) return ctx.ptrSize;
|
||||
if (field.isUnion) return unionNaturalAlignment(field, ctx);
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
// Bitfield alignment is determined by its storage type
|
||||
auto it = ctx.typeTable.find(field.typeName);
|
||||
if (it != ctx.typeTable.end()) return alignmentFor(it->kind);
|
||||
return 4; // default bitfield alignment
|
||||
}
|
||||
auto it = ctx.typeTable.find(field.typeName);
|
||||
if (it != ctx.typeTable.end()) return alignmentFor(it->kind);
|
||||
// Unknown type (struct reference) — align to pointer size
|
||||
return ctx.ptrSize;
|
||||
}
|
||||
|
||||
static inline int alignUp(int offset, int align) {
|
||||
return (offset + align - 1) & ~(align - 1);
|
||||
}
|
||||
|
||||
// Look up the byte size of a struct type (from already-built tree or static_assert declarations)
|
||||
static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
|
||||
auto classIt = ctx.classIds.find(typeName);
|
||||
if (classIt != ctx.classIds.end()) {
|
||||
int span = ctx.tree.structSpan(classIt.value());
|
||||
if (span > 0) {
|
||||
// Pad to struct's declared alignment (ALIGN(N))
|
||||
auto alignIt = ctx.structAlignments.find(typeName);
|
||||
if (alignIt != ctx.structAlignments.end() && *alignIt > 1)
|
||||
span = alignUp(span, *alignIt);
|
||||
return span;
|
||||
}
|
||||
}
|
||||
auto sizeIt = ctx.sizeAsserts.find(typeName);
|
||||
if (sizeIt != ctx.sizeAsserts.end())
|
||||
return sizeIt.value();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
const QVector<ParsedField>& fields) {
|
||||
int computedOffset = 0;
|
||||
@@ -959,8 +1199,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int bfAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, bfAlign);
|
||||
groupOffset = computedOffset;
|
||||
}
|
||||
int startIdx = fi;
|
||||
int totalBits = 0;
|
||||
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
|
||||
@@ -982,8 +1225,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int unionOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
unionOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int uAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, uAlign);
|
||||
unionOffset = computedOffset;
|
||||
}
|
||||
|
||||
Node unionNode;
|
||||
unionNode.kind = NodeKind::Struct;
|
||||
@@ -1013,8 +1259,11 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int fieldOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
else {
|
||||
int fAlign = fieldNaturalAlignment(field, ctx);
|
||||
computedOffset = alignUp(computedOffset, fAlign);
|
||||
fieldOffset = computedOffset;
|
||||
}
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = ctx.typeTable.find(field.typeName);
|
||||
@@ -1022,8 +1271,27 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
NodeKind ptrKind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
|
||||
// Array of pointers: PVOID arr[N]
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = ptrKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * ctx.ptrSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = (ctx.ptrSize >= 8) ? NodeKind::Pointer64 : NodeKind::Pointer32;
|
||||
n.kind = ptrKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
@@ -1098,7 +1366,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
field.typeName == QStringLiteral("char") && firstDim <= 128) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
@@ -1111,7 +1379,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR")) &&
|
||||
firstDim <= 128) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
@@ -1165,6 +1434,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
// Struct-type field
|
||||
if (isStructType) {
|
||||
int elemSize = structTypeSize(field.typeName, ctx);
|
||||
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
@@ -1182,6 +1453,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
if (elemSize > 0)
|
||||
computedOffset = fieldOffset + totalElements * elemSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1196,6 +1469,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
if (elemSize > 0)
|
||||
computedOffset = fieldOffset + elemSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1271,7 +1546,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
|
||||
enumNames.insert(ps.name);
|
||||
}
|
||||
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize};
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames, pointerSize, parser.sizeAsserts, parser.structAlignments};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
|
||||
362
src/main.cpp
362
src/main.cpp
@@ -509,7 +509,19 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
|
||||
qApp->setPalette(pal);
|
||||
|
||||
qApp->setStyleSheet(QString());
|
||||
// Global scrollbar styling — track matches control bg, handle is solid
|
||||
qApp->setStyleSheet(QStringLiteral(
|
||||
"QScrollBar:vertical { background: palette(window); width: 12px; margin: 0; border: none; }"
|
||||
"QScrollBar::handle:vertical { background: %1; min-height: 20px; border: none; }"
|
||||
"QScrollBar::handle:vertical:hover { background: %2; }"
|
||||
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"
|
||||
"QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; }"
|
||||
"QScrollBar:horizontal { background: palette(window); height: 12px; margin: 0; border: none; }"
|
||||
"QScrollBar::handle:horizontal { background: %1; min-width: 20px; border: none; }"
|
||||
"QScrollBar::handle:horizontal:hover { background: %2; }"
|
||||
"QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }"
|
||||
"QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { background: none; }")
|
||||
.arg(theme.textFaint.name(), theme.textDim.name()));
|
||||
}
|
||||
|
||||
class BorderOverlay : public QWidget {
|
||||
@@ -806,17 +818,17 @@ void MainWindow::createMenus() {
|
||||
|
||||
auto* actTreeLines = view->addAction("&Tree Lines");
|
||||
actTreeLines->setCheckable(true);
|
||||
actTreeLines->setChecked(settings.value("treeLines", false).toBool());
|
||||
actTreeLines->setChecked(settings.value("treeLines", true).toBool());
|
||||
connect(actTreeLines, &QAction::triggered, this, [this](bool checked) {
|
||||
QSettings("Reclass", "Reclass").setValue("treeLines", checked);
|
||||
for (auto& tab : m_tabs)
|
||||
tab.ctrl->setTreeLines(checked);
|
||||
});
|
||||
|
||||
auto* actRelOfs = view->addAction("R&elative Offsets");
|
||||
actRelOfs->setCheckable(true);
|
||||
actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
|
||||
connect(actRelOfs, &QAction::triggered, this, [this](bool checked) {
|
||||
m_actRelOfs = view->addAction("R&elative Offsets");
|
||||
m_actRelOfs->setCheckable(true);
|
||||
m_actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
|
||||
connect(m_actRelOfs, &QAction::triggered, this, [this](bool checked) {
|
||||
QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked);
|
||||
for (auto& tab : m_tabs)
|
||||
for (auto& pane : tab.panes)
|
||||
@@ -1218,22 +1230,36 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
// Style to match the top dock tab bar, with accent line on selected tab
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString editorFont = s.value("font", "JetBrains Mono").toString();
|
||||
pane.tabWidget->setStyleSheet(QStringLiteral(
|
||||
"QTabBar { border: none; }"
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;"
|
||||
" font-family: '%7'; font-size: 10pt;"
|
||||
"}"
|
||||
"QTabBar::tab:selected { color: %3; background: %4;"
|
||||
" border-top: 3px solid %6; padding-top: -3px; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||
.arg(t.background.name(), t.textMuted.name(), t.text.name(),
|
||||
t.backgroundAlt.name(), t.hover.name(), t.indHoverSpan.name()));
|
||||
t.backgroundAlt.name(), t.hover.name(), t.indHoverSpan.name(),
|
||||
editorFont));
|
||||
}
|
||||
|
||||
// Create editor via controller (parent = tabWidget for ownership)
|
||||
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
|
||||
pane.editor->setRelativeOffsets(
|
||||
QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool());
|
||||
// Sync View menu checkbox when editor toggles offset mode (double-click / context menu)
|
||||
connect(pane.editor, &RcxEditor::relativeOffsetsChanged, this, [this](bool rel) {
|
||||
QSettings("Reclass", "Reclass").setValue("relativeOffsets", rel);
|
||||
if (m_actRelOfs) m_actRelOfs->setChecked(rel);
|
||||
// Propagate to all other editors so they stay in sync
|
||||
for (auto& tab : m_tabs)
|
||||
for (auto& p : tab.panes)
|
||||
if (p.editor && p.editor != sender())
|
||||
p.editor->setRelativeOffsets(rel);
|
||||
});
|
||||
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
|
||||
|
||||
// Create per-pane rendered C++ view with find bar
|
||||
@@ -1441,13 +1467,13 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
splitter->setHandleWidth(1);
|
||||
auto* ctrl = new RcxController(doc, splitter);
|
||||
|
||||
QString title = doc->filePath.isEmpty()
|
||||
? rootName(doc->tree) : QFileInfo(doc->filePath).fileName();
|
||||
QString title = rootName(doc->tree);
|
||||
auto* dock = new QDockWidget(title, this);
|
||||
dock->setObjectName(QStringLiteral("DocDock_%1").arg(quintptr(dock), 0, 16));
|
||||
dock->setFeatures(QDockWidget::DockWidgetClosable |
|
||||
QDockWidget::DockWidgetMovable |
|
||||
QDockWidget::DockWidgetFloatable);
|
||||
dock->setAttribute(Qt::WA_DeleteOnClose);
|
||||
// Two title bar widgets: a hidden one (docked) and a draggable one (floating)
|
||||
auto* emptyTitleBar = new QWidget(dock);
|
||||
emptyTitleBar->setFixedHeight(0);
|
||||
@@ -1532,7 +1558,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
dockGrip->hide();
|
||||
|
||||
// Swap title bar when floating/docking, show/hide border + grip
|
||||
connect(dock, &QDockWidget::topLevelChanged, this, [dock, emptyTitleBar, floatTitleBar, dockBorder, dockGrip](bool floating) {
|
||||
connect(dock, &QDockWidget::topLevelChanged, this, [this, dock, emptyTitleBar, floatTitleBar, dockBorder, dockGrip](bool floating) {
|
||||
dock->setTitleBarWidget(floating ? floatTitleBar : emptyTitleBar);
|
||||
if (floating) {
|
||||
dockBorder->setGeometry(0, 0, dock->width(), dock->height());
|
||||
@@ -1544,6 +1570,8 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
} else {
|
||||
dockBorder->hide();
|
||||
dockGrip->hide();
|
||||
// Re-docking creates a new tab bar — reinstall pin/close buttons
|
||||
QTimer::singleShot(0, this, [this]() { setupDockTabBars(); });
|
||||
}
|
||||
});
|
||||
dock->installEventFilter(new DockBorderFilter(dockBorder, dockGrip, dock));
|
||||
@@ -1569,7 +1597,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
|
||||
// Apply global compact columns setting to new tab
|
||||
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
|
||||
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", false).toBool());
|
||||
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", true).toBool());
|
||||
ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool());
|
||||
|
||||
// Give every controller the shared document list for cross-tab type visibility
|
||||
@@ -1581,7 +1609,6 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
if (visible) {
|
||||
m_activeDocDock = dock;
|
||||
updateWindowTitle();
|
||||
rebuildWorkspaceModel();
|
||||
// Sync view toggle buttons to this tab's active pane
|
||||
auto it = m_tabs.find(dock);
|
||||
if (it != m_tabs.end()) {
|
||||
@@ -1613,6 +1640,8 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
|
||||
rebuildAllDocs();
|
||||
rebuildWorkspaceModel();
|
||||
if (m_tabs.isEmpty())
|
||||
project_new();
|
||||
});
|
||||
|
||||
connect(ctrl, &RcxController::nodeSelected,
|
||||
@@ -1667,8 +1696,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
auto it2 = m_tabs.find(dockGuard);
|
||||
if (it2 != m_tabs.end()) {
|
||||
updateAllRenderedPanes(*it2);
|
||||
if (it2->doc->filePath.isEmpty())
|
||||
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
}
|
||||
rebuildWorkspaceModel();
|
||||
updateWindowTitle();
|
||||
@@ -1684,8 +1712,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
auto it2 = m_tabs.find(dockGuard);
|
||||
if (it2 != m_tabs.end()) {
|
||||
updateAllRenderedPanes(*it2);
|
||||
if (it2->doc->filePath.isEmpty())
|
||||
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
dockGuard->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
}
|
||||
updateWindowTitle();
|
||||
rebuildWorkspaceModel();
|
||||
@@ -1734,6 +1761,7 @@ void MainWindow::setupDockTabBars() {
|
||||
|
||||
// No stylesheet — painting handled by MenuBarStyle
|
||||
tabBar->setStyleSheet(QString());
|
||||
tabBar->setAttribute(Qt::WA_Hover, true);
|
||||
tabBar->setElideMode(Qt::ElideNone);
|
||||
tabBar->setExpanding(false);
|
||||
// Set editor font so tab width sizing matches our label painting
|
||||
@@ -1774,8 +1802,9 @@ void MainWindow::setupDockTabBars() {
|
||||
tabBar->setTabButton(i, QTabBar::RightSide, btns);
|
||||
}
|
||||
|
||||
// Context menu (install only once)
|
||||
// Middle-click close + context menu (install only once)
|
||||
if (tabBar->contextMenuPolicy() == Qt::CustomContextMenu) continue;
|
||||
tabBar->installEventFilter(this);
|
||||
tabBar->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(tabBar, &QTabBar::customContextMenuRequested,
|
||||
this, [this, tabBar](const QPoint& pos) {
|
||||
@@ -1790,9 +1819,6 @@ void MainWindow::setupDockTabBars() {
|
||||
if (!target) return;
|
||||
|
||||
auto tabIt = m_tabs.find(target);
|
||||
auto* btns = qobject_cast<DockTabButtons*>(
|
||||
tabBar->tabButton(idx, QTabBar::RightSide));
|
||||
bool isPinned = btns && btns->pinned;
|
||||
|
||||
QMenu menu;
|
||||
|
||||
@@ -1816,28 +1842,6 @@ void MainWindow::setupDockTabBars() {
|
||||
});
|
||||
}
|
||||
|
||||
// Close All But Pinned (only if any tab is pinned)
|
||||
bool anyPinned = false;
|
||||
for (int i = 0; i < tabBar->count(); ++i) {
|
||||
auto* b = qobject_cast<DockTabButtons*>(
|
||||
tabBar->tabButton(i, QTabBar::RightSide));
|
||||
if (b && b->pinned) { anyPinned = true; break; }
|
||||
}
|
||||
if (anyPinned) {
|
||||
menu.addAction("Close All But Pinned", [this, tabBar]() {
|
||||
QVector<QDockWidget*> toClose;
|
||||
for (int i = 0; i < tabBar->count(); ++i) {
|
||||
auto* b = qobject_cast<DockTabButtons*>(
|
||||
tabBar->tabButton(i, QTabBar::RightSide));
|
||||
if (b && b->pinned) continue;
|
||||
QString title = tabBar->tabText(i);
|
||||
for (auto* d : m_docDocks)
|
||||
if (d->windowTitle() == title) { toClose.append(d); break; }
|
||||
}
|
||||
for (auto* d : toClose) d->close();
|
||||
});
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// Copy Full Path / Open Containing Folder (only if saved)
|
||||
@@ -1859,14 +1863,6 @@ void MainWindow::setupDockTabBars() {
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// Pin / Unpin
|
||||
if (btns) {
|
||||
QIcon pinIcon = makeIcon(isPinned ? ":/vsicons/pinned.svg"
|
||||
: ":/vsicons/pin.svg");
|
||||
menu.addAction(pinIcon, isPinned ? "Unpin Tab" : "Pin Tab",
|
||||
[btns, isPinned]() { btns->setPinned(!isPinned); });
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// New Document Groups (only if >1 tab)
|
||||
@@ -1918,8 +1914,47 @@ void MainWindow::setupDockTabBars() {
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
auto* me = static_cast<QMouseEvent*>(event);
|
||||
if (me->button() == Qt::MiddleButton) {
|
||||
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
|
||||
int idx = tabBar->tabAt(me->pos());
|
||||
if (idx >= 0) {
|
||||
QString title = tabBar->tabText(idx);
|
||||
for (auto* d : m_docDocks) {
|
||||
if (d->windowTitle() == title) { d->close(); break; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return QMainWindow::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
// Build a minimal empty struct for new documents
|
||||
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
|
||||
// ── Enum: bare node with empty enumMembers, no hex children ──
|
||||
if (classKeyword == QStringLiteral("enum")) {
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Unnamed";
|
||||
root.structTypeName = "Unnamed";
|
||||
root.classKeyword = classKeyword;
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
root.enumMembers = {
|
||||
{QStringLiteral("Member0"), 0},
|
||||
{QStringLiteral("Member1"), 1},
|
||||
{QStringLiteral("Member2"), 2},
|
||||
{QStringLiteral("Member3"), 3},
|
||||
{QStringLiteral("Member4"), 4},
|
||||
};
|
||||
tree.addNode(root);
|
||||
return;
|
||||
}
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "instance";
|
||||
@@ -2292,10 +2327,6 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
|
||||
// Dock separator is 1px via PM_DockWidgetSeparatorExtent in MenuBarStyle
|
||||
|
||||
// Custom title bar
|
||||
if (m_titleBar)
|
||||
m_titleBar->applyTheme(theme);
|
||||
|
||||
// Start page
|
||||
if (m_startPage)
|
||||
m_startPage->applyTheme(theme);
|
||||
@@ -2311,12 +2342,18 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
"QDockWidget { border: none; }"
|
||||
"QDockWidget > QWidget { border: none; }"));
|
||||
|
||||
// Custom title bar — applied AFTER setStyleSheet() because the MainWindow
|
||||
// stylesheet re-resolves descendant palettes and would reset the QMenuBar palette.
|
||||
if (m_titleBar)
|
||||
m_titleBar->applyTheme(theme);
|
||||
|
||||
for (auto* tabBar : findChildren<QTabBar*>()) {
|
||||
// Only style tab bars owned directly by this QMainWindow (dock tabs),
|
||||
// skip ones inside SplitPane QTabWidgets etc.
|
||||
if (tabBar->parent() == this) {
|
||||
// No stylesheet — painting handled by MenuBarStyle (CE_TabBarTabShape/Label)
|
||||
tabBar->setStyleSheet(QString());
|
||||
tabBar->setAttribute(Qt::WA_Hover, true);
|
||||
tabBar->setElideMode(Qt::ElideNone);
|
||||
tabBar->setExpanding(false);
|
||||
// Set editor font so tab width sizing matches our label painting
|
||||
@@ -2345,16 +2382,19 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
|
||||
// Restyle per-pane view tab bars (Reclass / C++)
|
||||
{
|
||||
QString editorFont = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
QString paneTabStyle = QStringLiteral(
|
||||
"QTabBar { border: none; }"
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;"
|
||||
" font-family: '%7'; font-size: 10pt;"
|
||||
"}"
|
||||
"QTabBar::tab:selected { color: %3; background: %4;"
|
||||
" border-top: 3px solid %6; padding-top: -3px; }"
|
||||
"QTabBar::tab:hover { color: %3; background: %5; }")
|
||||
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
|
||||
theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name());
|
||||
theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name(),
|
||||
editorFont);
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||
for (auto& pane : it->panes) {
|
||||
if (pane.tabWidget)
|
||||
@@ -2380,19 +2420,30 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
||||
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
||||
|
||||
// Workspace tree: colors from theme (selection + text)
|
||||
// Workspace tree: delegate colors, palette, stylesheet
|
||||
if (m_workspaceDelegate)
|
||||
m_workspaceDelegate->setThemeColors(theme);
|
||||
if (m_workspaceTree) {
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, theme.textDim);
|
||||
tp.setColor(QPalette::Highlight, theme.hover);
|
||||
tp.setColor(QPalette::Highlight, theme.selected);
|
||||
tp.setColor(QPalette::HighlightedText, theme.text);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||
"QTreeView { background: %1; border: none; }"
|
||||
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }")
|
||||
.arg(theme.background.name()));
|
||||
m_workspaceTree->viewport()->update();
|
||||
}
|
||||
if (m_workspaceSearch) {
|
||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: none;"
|
||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
||||
.arg(theme.background.name(), theme.textDim.name(), theme.border.name()));
|
||||
" padding: 4px 8px; }"
|
||||
"QLineEdit QToolButton { padding: 0px 4px; }"
|
||||
"QLineEdit QToolButton:hover { background: %3; }")
|
||||
.arg(theme.background.name(), theme.textDim.name(),
|
||||
theme.hover.name()));
|
||||
}
|
||||
|
||||
// Dock titlebar: restyle via palette + close button
|
||||
@@ -2413,6 +2464,9 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
if (m_dockGrip)
|
||||
m_dockGrip->setGripColor(theme.textFaint);
|
||||
if (m_workspaceDock)
|
||||
m_workspaceDock->setStyleSheet(QStringLiteral(
|
||||
"QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
|
||||
|
||||
// Scanner dock
|
||||
if (m_scannerPanel)
|
||||
@@ -2480,10 +2534,11 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Identifier);
|
||||
lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
||||
const QColor editorBg = theme.background.darker(115);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
lexer->setPaper(theme.background, i);
|
||||
lexer->setPaper(editorBg, i);
|
||||
}
|
||||
sci->setPaper(theme.background);
|
||||
sci->setPaper(theme.background.darker(115));
|
||||
sci->setColor(theme.text);
|
||||
sci->setCaretForegroundColor(theme.text);
|
||||
sci->setCaretLineBackgroundColor(theme.hover);
|
||||
@@ -2582,12 +2637,17 @@ 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 workspace tree, title, and search font (10pt monospace)
|
||||
{
|
||||
QFont wf(fontName, 10);
|
||||
wf.setFixedPitch(true);
|
||||
if (m_workspaceTree)
|
||||
m_workspaceTree->setFont(wf);
|
||||
if (m_dockTitleLabel)
|
||||
m_dockTitleLabel->setFont(wf);
|
||||
if (m_workspaceSearch)
|
||||
m_workspaceSearch->setFont(wf);
|
||||
}
|
||||
// Sync scanner panel font
|
||||
if (m_scannerPanel)
|
||||
m_scannerPanel->setEditorFont(f);
|
||||
@@ -2608,6 +2668,9 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
||||
tabBar->update();
|
||||
}
|
||||
}
|
||||
// Pane tab bars (Reclass / C++) — re-apply stylesheet with new font
|
||||
// (stylesheet overrides setFont, so font must be in the CSS)
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2639,9 +2702,7 @@ void MainWindow::updateWindowTitle() {
|
||||
auto* activeDock = m_activeDocDock;
|
||||
if (activeDock && m_tabs.contains(activeDock)) {
|
||||
auto& tab = m_tabs[activeDock];
|
||||
QString name = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
||||
if (tab.doc->modified) name += " *";
|
||||
title = name + " - Reclass";
|
||||
} else {
|
||||
@@ -2695,15 +2756,16 @@ void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Identifier);
|
||||
lexer->setColor(theme.syntaxPreproc, QsciLexerCPP::PreProcessor);
|
||||
lexer->setColor(theme.text, QsciLexerCPP::Operator);
|
||||
const QColor editorBg = theme.background.darker(115);
|
||||
for (int i = 0; i <= 127; i++) {
|
||||
lexer->setPaper(theme.background, i);
|
||||
lexer->setPaper(editorBg, i);
|
||||
lexer->setFont(f, i);
|
||||
}
|
||||
sci->setLexer(lexer);
|
||||
sci->setBraceMatching(QsciScintilla::NoBraceMatch);
|
||||
|
||||
// Colors applied AFTER setLexer() — the lexer resets these on attach
|
||||
sci->setPaper(theme.background);
|
||||
sci->setPaper(editorBg);
|
||||
sci->setColor(theme.text);
|
||||
sci->setCaretForegroundColor(theme.text);
|
||||
sci->setCaretLineVisible(true);
|
||||
@@ -2959,7 +3021,7 @@ void MainWindow::importFromSource() {
|
||||
rebuildWorkspaceModel();
|
||||
if (!m_docDocks.isEmpty()) {
|
||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {220}, Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {128}, Qt::Horizontal);
|
||||
}
|
||||
m_workspaceDock->show();
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||
@@ -3013,7 +3075,7 @@ void MainWindow::importPdb() {
|
||||
rebuildWorkspaceModel();
|
||||
if (!m_docDocks.isEmpty()) {
|
||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {220}, Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {128}, Qt::Horizontal);
|
||||
}
|
||||
m_workspaceDock->show();
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
@@ -3204,7 +3266,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
||||
rebuildWorkspaceModel();
|
||||
if (!m_docDocks.isEmpty()) {
|
||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {220}, Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {128}, Qt::Horizontal);
|
||||
}
|
||||
m_workspaceDock->show();
|
||||
int classCount = 0;
|
||||
@@ -3230,7 +3292,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
||||
rebuildWorkspaceModel();
|
||||
if (!m_docDocks.isEmpty()) {
|
||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {220}, Qt::Horizontal);
|
||||
resizeDocks({m_workspaceDock}, {128}, Qt::Horizontal);
|
||||
}
|
||||
m_workspaceDock->show();
|
||||
addRecentFile(filePath);
|
||||
@@ -3260,16 +3322,13 @@ void MainWindow::project_close(QDockWidget* dock) {
|
||||
if (!dock) dock = m_activeDocDock;
|
||||
if (!dock) return;
|
||||
dock->close();
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
|
||||
void MainWindow::closeAllDocDocks() {
|
||||
// Take a copy since closing modifies m_docDocks via destroyed signal
|
||||
auto docks = m_docDocks;
|
||||
for (auto* dock : docks) {
|
||||
dock->setAttribute(Qt::WA_DeleteOnClose);
|
||||
for (auto* dock : docks)
|
||||
dock->close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3286,7 +3345,7 @@ void MainWindow::createWorkspaceDock() {
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
|
||||
auto* titleBar = new QWidget(m_workspaceDock);
|
||||
titleBar->setFixedHeight(24);
|
||||
titleBar->setFixedHeight(26);
|
||||
titleBar->setAutoFillBackground(true);
|
||||
{
|
||||
QPalette tbPal = titleBar->palette();
|
||||
@@ -3305,6 +3364,10 @@ void MainWindow::createWorkspaceDock() {
|
||||
QPalette lp = m_dockTitleLabel->palette();
|
||||
lp.setColor(QPalette::WindowText, t.textDim);
|
||||
m_dockTitleLabel->setPalette(lp);
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||
f.setFixedPitch(true);
|
||||
m_dockTitleLabel->setFont(f);
|
||||
}
|
||||
layout->addWidget(m_dockTitleLabel);
|
||||
|
||||
@@ -3324,6 +3387,13 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceDock->setTitleBarWidget(titleBar);
|
||||
}
|
||||
|
||||
// Outer border around entire dock (header + search + tree)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_workspaceDock->setStyleSheet(QStringLiteral(
|
||||
"QDockWidget { border: 1px solid %1; }").arg(t.border.name()));
|
||||
}
|
||||
|
||||
// Container widget: search box + tree view
|
||||
auto* dockContainer = new QWidget(m_workspaceDock);
|
||||
auto* dockLayout = new QVBoxLayout(dockContainer);
|
||||
@@ -3332,13 +3402,52 @@ void MainWindow::createWorkspaceDock() {
|
||||
|
||||
m_workspaceSearch = new QLineEdit(dockContainer);
|
||||
m_workspaceSearch->setPlaceholderText(QStringLiteral("Search..."));
|
||||
m_workspaceSearch->setClearButtonEnabled(true);
|
||||
// Clear button uses our close.svg icon instead of Qt's default circle-X
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||
f.setFixedPitch(true);
|
||||
m_workspaceSearch->setFont(f);
|
||||
}
|
||||
{
|
||||
auto* searchAction = m_workspaceSearch->addAction(
|
||||
QIcon(QStringLiteral(":/vsicons/search.svg")),
|
||||
QLineEdit::LeadingPosition);
|
||||
// Find the QToolButton created for the action and shrink its icon
|
||||
for (auto* btn : m_workspaceSearch->findChildren<QToolButton*>()) {
|
||||
if (btn->defaultAction() == searchAction) {
|
||||
btn->setIconSize(QSize(14, 14));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
auto* clearAction = m_workspaceSearch->addAction(
|
||||
QIcon(QStringLiteral(":/vsicons/close.svg")),
|
||||
QLineEdit::TrailingPosition);
|
||||
clearAction->setVisible(false);
|
||||
connect(clearAction, &QAction::triggered,
|
||||
m_workspaceSearch, &QLineEdit::clear);
|
||||
connect(m_workspaceSearch, &QLineEdit::textChanged,
|
||||
clearAction, [clearAction](const QString& text) {
|
||||
clearAction->setVisible(!text.isEmpty());
|
||||
});
|
||||
for (auto* btn : m_workspaceSearch->findChildren<QToolButton*>()) {
|
||||
if (btn->defaultAction() == clearAction) {
|
||||
btn->setIconSize(QSize(14, 14));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_workspaceSearch->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: none;"
|
||||
" border-bottom: 1px solid %3; padding: 4px 6px; }")
|
||||
.arg(t.background.name(), t.textDim.name(), t.border.name()));
|
||||
" padding: 4px 8px; }"
|
||||
"QLineEdit QToolButton { padding: 0px 4px; }"
|
||||
"QLineEdit QToolButton:hover { background: %3; }")
|
||||
.arg(t.background.name(), t.textDim.name(),
|
||||
t.hover.name()));
|
||||
}
|
||||
dockLayout->addWidget(m_workspaceSearch);
|
||||
|
||||
@@ -3356,23 +3465,37 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||
f.setFixedPitch(true);
|
||||
m_workspaceTree->setFont(f);
|
||||
}
|
||||
|
||||
connect(m_workspaceSearch, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||
m_workspaceProxy->setFilterFixedString(text);
|
||||
if (!text.isEmpty())
|
||||
m_workspaceTree->expandAll();
|
||||
else
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
});
|
||||
|
||||
// Override palette: selection + hover use theme colors (not default blue)
|
||||
// Custom delegate for rich text rendering (name bright, metadata dim)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_workspaceDelegate = new rcx::WorkspaceDelegate(m_workspaceTree);
|
||||
m_workspaceDelegate->setThemeColors(t);
|
||||
m_workspaceTree->setItemDelegate(m_workspaceDelegate);
|
||||
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, t.textDim);
|
||||
tp.setColor(QPalette::Highlight, t.hover);
|
||||
tp.setColor(QPalette::Highlight, t.selected);
|
||||
tp.setColor(QPalette::HighlightedText, t.text);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
|
||||
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||
"QTreeView { background: %1; border: none; }"
|
||||
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }")
|
||||
.arg(t.background.name()));
|
||||
}
|
||||
|
||||
dockLayout->addWidget(m_workspaceTree);
|
||||
@@ -3380,13 +3503,9 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||
if (!index.isValid()) return;
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
|
||||
// Right-click on "Project" group → New Class / New Struct / New Enum
|
||||
if (structId == rcx::kGroupSentinel) {
|
||||
// Right-click on empty area → New Class / New Struct / New Enum
|
||||
if (!index.isValid()) {
|
||||
QMenu menu;
|
||||
auto* actClass = menu.addAction("New Class");
|
||||
auto* actStruct = menu.addAction("New Struct");
|
||||
@@ -3398,6 +3517,8 @@ void MainWindow::createWorkspaceDock() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
if (structId == 0) return;
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
@@ -3502,12 +3623,6 @@ void MainWindow::createWorkspaceDock() {
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
|
||||
if (structId == rcx::kGroupSentinel) {
|
||||
// "Project" folder: toggle expand/collapse
|
||||
m_workspaceTree->setExpanded(index, !m_workspaceTree->isExpanded(index));
|
||||
return;
|
||||
}
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) return;
|
||||
auto* ownerDock = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||
@@ -3707,19 +3822,47 @@ void MainWindow::rebuildAllDocs() {
|
||||
}
|
||||
|
||||
void MainWindow::rebuildWorkspaceModel() {
|
||||
// Debounce: coalesce rapid calls into a single rebuild
|
||||
if (!m_workspaceRebuildTimer) {
|
||||
m_workspaceRebuildTimer = new QTimer(this);
|
||||
m_workspaceRebuildTimer->setSingleShot(true);
|
||||
m_workspaceRebuildTimer->setInterval(50);
|
||||
connect(m_workspaceRebuildTimer, &QTimer::timeout,
|
||||
this, &MainWindow::rebuildWorkspaceModelNow);
|
||||
}
|
||||
m_workspaceRebuildTimer->start();
|
||||
}
|
||||
|
||||
void MainWindow::rebuildWorkspaceModelNow() {
|
||||
QVector<rcx::TabInfo> tabs;
|
||||
QSet<RcxDocument*> seenDocs;
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||
TabState& tab = it.value();
|
||||
if (seenDocs.contains(tab.doc)) continue; // skip duplicate doc views
|
||||
if (seenDocs.contains(tab.doc)) continue;
|
||||
seenDocs.insert(tab.doc);
|
||||
QString name = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||
}
|
||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
rcx::syncProjectExplorer(m_workspaceModel, tabs);
|
||||
|
||||
if (m_dockTitleLabel) {
|
||||
int structs = 0, enums = 0;
|
||||
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
|
||||
if (m_workspaceModel->item(i)->data(Qt::UserRole + 2).toBool())
|
||||
++enums;
|
||||
else
|
||||
++structs;
|
||||
}
|
||||
QString title = QStringLiteral("Project");
|
||||
if (structs || enums) {
|
||||
title += QStringLiteral(" \u2014 %1 struct%2")
|
||||
.arg(structs).arg(structs != 1 ? "s" : "");
|
||||
if (enums)
|
||||
title += QStringLiteral(" \u00b7 %1 enum%2")
|
||||
.arg(enums).arg(enums != 1 ? "s" : "");
|
||||
}
|
||||
m_dockTitleLabel->setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::addRecentFile(const QString& path) {
|
||||
@@ -4010,9 +4153,10 @@ void MainWindow::showStartPage() {
|
||||
|
||||
void MainWindow::dismissStartPage() {
|
||||
if (!m_startPage) return;
|
||||
m_startPage->close();
|
||||
m_startPage->deleteLater();
|
||||
m_startPage = nullptr;
|
||||
auto* sp = m_startPage;
|
||||
m_startPage = nullptr; // null first — close() may re-enter via rejected signal
|
||||
sp->close();
|
||||
sp->deleteLater();
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace rcx {
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
class DockGripWidget;
|
||||
class WorkspaceDelegate;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -91,6 +92,7 @@ private:
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QAction* m_actRelOfs = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
QMenu* m_recentFilesMenu = nullptr;
|
||||
|
||||
@@ -155,11 +157,14 @@ private:
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QSortFilterProxyModel* m_workspaceProxy = nullptr;
|
||||
QLineEdit* m_workspaceSearch = nullptr;
|
||||
WorkspaceDelegate* m_workspaceDelegate = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
DockGripWidget* m_dockGrip = nullptr;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel();
|
||||
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
||||
void rebuildWorkspaceModelNow(); // immediate rebuild
|
||||
QTimer* m_workspaceRebuildTimer = nullptr;
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
// Scanner dock
|
||||
@@ -178,6 +183,7 @@ private:
|
||||
protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -447,6 +447,22 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
|
||||
// process.info
|
||||
tools.append(QJsonObject{
|
||||
{"name", "process.info"},
|
||||
{"description", "Returns PEB address and enumerates all Thread Environment Blocks (TEBs) for the attached process. "
|
||||
"TEBs are discovered via NtQuerySystemInformation and NtQueryInformationThread. "
|
||||
"Each TEB entry includes: address, threadId. "
|
||||
"Requires a live process provider with PEB support."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index (0-based). Omit for active tab."}}}
|
||||
}}
|
||||
}}
|
||||
});
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -472,6 +488,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else if (toolName == "node.history") result = toolNodeHistory(args);
|
||||
else if (toolName == "process.info") result = toolProcessInfo(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
@@ -1327,6 +1344,39 @@ QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
|
||||
QJsonDocument(result).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// TOOL: process.info — PEB address + TEB enumeration
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab", true);
|
||||
|
||||
auto* prov = tab->doc->provider.get();
|
||||
if (!prov) return makeTextResult("No data source attached", true);
|
||||
if (!prov->isLive()) return makeTextResult("Not a live provider", true);
|
||||
|
||||
uint64_t pebAddr = prov->peb();
|
||||
if (!pebAddr) return makeTextResult("PEB not available for this provider", true);
|
||||
|
||||
QJsonObject out;
|
||||
out["peb"] = "0x" + QString::number(pebAddr, 16).toUpper();
|
||||
|
||||
auto tebList = prov->tebs();
|
||||
QJsonArray tebArr;
|
||||
for (const auto& t : tebList) {
|
||||
tebArr.append(QJsonObject{
|
||||
{"address", "0x" + QString::number(t.tebAddress, 16).toUpper()},
|
||||
{"threadId", (qint64)t.threadId}
|
||||
});
|
||||
}
|
||||
|
||||
out["tebs"] = tebArr;
|
||||
out["tebCount"] = tebArr.size();
|
||||
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -60,6 +60,7 @@ private:
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
QJsonObject toolProcessInfo(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -73,6 +73,13 @@ public:
|
||||
// Default: returns empty (scan engine falls back to [0, size())).
|
||||
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
|
||||
|
||||
// Process Environment Block address (x64 PEB VA in target process).
|
||||
// Only meaningful for live process providers. Returns 0 if unavailable.
|
||||
virtual uint64_t peb() const { return 0; }
|
||||
|
||||
struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; };
|
||||
virtual QVector<ThreadInfo> tebs() const { return {}; }
|
||||
|
||||
// --- Derived convenience (non-virtual, never override) ---
|
||||
|
||||
bool isValid() const { return size() > 0; }
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
|
||||
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
|
||||
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
|
||||
<file alias="chevron-right.svg">vsicons/chevron-right.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file>
|
||||
|
||||
32
src/themes/defaults/modern.json
Normal file
32
src/themes/defaults/modern.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Modern",
|
||||
"background": "#0e1117",
|
||||
"backgroundAlt": "#12151c",
|
||||
"surface": "#181d27",
|
||||
"border": "#1e2533",
|
||||
"borderFocused": "#4fc3f7",
|
||||
"button": "#1e2433",
|
||||
"text": "#a8bbd0",
|
||||
"textDim": "#7a8fa8",
|
||||
"textMuted": "#566278",
|
||||
"textFaint": "#3d4d6a",
|
||||
"hover": "#1e2433",
|
||||
"selected": "#232a3a",
|
||||
"selection": "#1a4a5e",
|
||||
"syntaxKeyword": "#9d8cff",
|
||||
"syntaxNumber": "#f0c060",
|
||||
"syntaxString": "#26c6b3",
|
||||
"syntaxComment": "#566278",
|
||||
"syntaxPreproc": "#f472b6",
|
||||
"syntaxType": "#4fc3f7",
|
||||
"indHoverSpan": "#f0c060",
|
||||
"indCmdPill": "#12151c",
|
||||
"indDataChanged": "#6bda8a",
|
||||
"indHeatCold": "#f0c060",
|
||||
"indHeatWarm": "#e8946a",
|
||||
"indHeatHot": "#ff6b6b",
|
||||
"indHintGreen": "#2a5e3a",
|
||||
"markerPtr": "#ff6b6b",
|
||||
"markerCycle": "#f0c060",
|
||||
"markerError": "#3a1a1a"
|
||||
}
|
||||
@@ -415,9 +415,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
return btn;
|
||||
};
|
||||
|
||||
m_chipPrim = makeChip(QStringLiteral("P"));
|
||||
m_chipTypes = makeChip(QStringLiteral("T"));
|
||||
m_chipEnums = makeChip(QStringLiteral("E"));
|
||||
m_chipPrim = makeChip(QStringLiteral("Built-in"));
|
||||
m_chipTypes = makeChip(QStringLiteral("Types"));
|
||||
m_chipEnums = makeChip(QStringLiteral("Enum"));
|
||||
m_chipPrim->setAccessibleName(QStringLiteral("Show primitives"));
|
||||
m_chipTypes->setAccessibleName(QStringLiteral("Show composites"));
|
||||
m_chipEnums->setAccessibleName(QStringLiteral("Show enums"));
|
||||
@@ -1080,9 +1080,9 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
auto updateChipLabel = [](QToolButton* btn, const QString& abbrev, int count) {
|
||||
btn->setText(QStringLiteral("%1 (%2)").arg(abbrev).arg(count));
|
||||
};
|
||||
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("P"), primCount);
|
||||
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("T"), typeCount);
|
||||
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("E"), enumCount);
|
||||
if (m_chipPrim) updateChipLabel(m_chipPrim, QStringLiteral("Built-in"), primCount);
|
||||
if (m_chipTypes) updateChipLabel(m_chipTypes, QStringLiteral("Types"), typeCount);
|
||||
if (m_chipEnums) updateChipLabel(m_chipEnums, QStringLiteral("Enum"), enumCount);
|
||||
|
||||
if (m_statusLabel)
|
||||
m_statusLabel->setText(QStringLiteral("%1 results").arg(resultCount));
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
@@ -13,22 +17,85 @@ struct TabInfo {
|
||||
void* subPtr; // QDockWidget* as void*
|
||||
};
|
||||
|
||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||
// Helper: is a Hex padding node
|
||||
inline bool isHexPad(NodeKind k) {
|
||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||
}
|
||||
|
||||
// Build child rows for a struct item.
|
||||
inline void buildStructChildren(QStandardItem* item,
|
||||
const NodeTree* tree, uint64_t structId,
|
||||
void* subPtr) {
|
||||
item->removeRows(0, item->rowCount());
|
||||
|
||||
QVector<int> members = tree->childrenOf(structId);
|
||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||
return tree->nodes[a].offset < tree->nodes[b].offset;
|
||||
});
|
||||
|
||||
auto memberTypeName = [](const Node& m) -> QString {
|
||||
if (m.kind == NodeKind::Struct) {
|
||||
return m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||
: m.structTypeName;
|
||||
}
|
||||
return QString::fromLatin1(kindToString(m.kind));
|
||||
};
|
||||
|
||||
for (int mi : members) {
|
||||
const Node& m = tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
QString childDisplay = QStringLiteral("%1 %2")
|
||||
.arg(memberTypeName(m), m.name);
|
||||
auto* childItem = new QStandardItem(childDisplay);
|
||||
childItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||
item->appendRow(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to build display string for a type entry.
|
||||
inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
if (node->resolvedClassKeyword() == QStringLiteral("enum")) {
|
||||
return QStringLiteral("%1 \u2014 %2")
|
||||
.arg(nameOf(node),
|
||||
QString::number(node->enumMembers.size()));
|
||||
}
|
||||
QVector<int> members = tree->childrenOf(node->id);
|
||||
int vc = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(tree->nodes[mi].kind)) ++vc;
|
||||
return QStringLiteral("%1 \u2014 %2")
|
||||
.arg(nameOf(node), QString::number(vc));
|
||||
}
|
||||
|
||||
// Build a new item for a type entry.
|
||||
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
|
||||
void* subPtr) {
|
||||
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(isEnum ? ":/vsicons/symbol-enum.svg"
|
||||
: ":/vsicons/symbol-structure.svg"),
|
||||
typeDisplayString(node, tree));
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
|
||||
item->setData(isEnum, Qt::UserRole + 2);
|
||||
|
||||
if (!isEnum)
|
||||
buildStructChildren(item, tree, node->id, subPtr);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Full rebuild — used by benchmarks and first build.
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
model->clear();
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
|
||||
// Single "Project" root with folder icon
|
||||
void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr;
|
||||
auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"),
|
||||
QStringLiteral("Project"));
|
||||
projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole);
|
||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
struct Entry { const Node* node; void* subPtr; const NodeTree* tree; };
|
||||
QVector<Entry> types, enums;
|
||||
for (const auto& tab : tabs) {
|
||||
@@ -43,78 +110,190 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
|
||||
// Helper: is a Hex padding node
|
||||
auto isHexPad = [](NodeKind k) {
|
||||
return k == NodeKind::Hex8 || k == NodeKind::Hex16
|
||||
|| k == NodeKind::Hex32 || k == NodeKind::Hex64;
|
||||
};
|
||||
|
||||
// Helper: type display string for a member node
|
||||
auto memberTypeName = [](const Node& m) -> QString {
|
||||
if (m.kind == NodeKind::Struct) {
|
||||
QString stn = m.structTypeName.isEmpty() ? m.resolvedClassKeyword()
|
||||
: m.structTypeName;
|
||||
return stn;
|
||||
}
|
||||
return QString::fromLatin1(kindToString(m.kind));
|
||||
};
|
||||
|
||||
// TODO: re-enable sorting once startup perf is acceptable
|
||||
// auto countVisible = [&](const Entry& e) { ... };
|
||||
// std::sort(types.begin(), types.end(), cmpChildren);
|
||||
// std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
for (const auto& e : types) {
|
||||
QVector<int> members = e.tree->childrenOf(e.node->id);
|
||||
|
||||
// Count non-hex members for display
|
||||
int visibleCount = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visibleCount;
|
||||
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(visibleCount));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
|
||||
// Add child rows sorted by offset (skip Hex padding)
|
||||
std::sort(members.begin(), members.end(), [&](int a, int b) {
|
||||
return e.tree->nodes[a].offset < e.tree->nodes[b].offset;
|
||||
});
|
||||
for (int mi : members) {
|
||||
const Node& m = e.tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
QString childDisplay = QStringLiteral("%1 %2")
|
||||
.arg(memberTypeName(m), m.name);
|
||||
auto* childItem = new QStandardItem(childDisplay);
|
||||
childItem->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
childItem->setData(QVariant::fromValue(m.id), Qt::UserRole + 1);
|
||||
item->appendRow(childItem);
|
||||
}
|
||||
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
for (const auto& e : enums) {
|
||||
int count = e.node->enumMembers.size();
|
||||
QString display = QStringLiteral("%1 (%2) \u2014 %3")
|
||||
.arg(nameOf(e.node), e.node->resolvedClassKeyword(),
|
||||
QString::number(count));
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(e.node->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
model->appendRow(projectItem);
|
||||
for (const auto& e : types)
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
for (const auto& e : enums)
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
}
|
||||
|
||||
// Incremental sync — preserves tree expansion/scroll state.
|
||||
inline void syncProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
// First call — full build
|
||||
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
||||
buildProjectExplorer(model, tabs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect desired entries
|
||||
struct Entry { uint64_t id; const Node* node; void* subPtr; const NodeTree* tree; bool isEnum; };
|
||||
QVector<Entry> desired;
|
||||
for (const auto& tab : tabs) {
|
||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||
for (int idx : topLevel) {
|
||||
const Node& n = tab.tree->nodes[idx];
|
||||
if (n.kind != NodeKind::Struct) continue;
|
||||
bool ie = n.resolvedClassKeyword() == QStringLiteral("enum");
|
||||
desired.append({n.id, &n, tab.subPtr, tab.tree, ie});
|
||||
}
|
||||
}
|
||||
|
||||
QHash<uint64_t, int> desiredMap;
|
||||
desiredMap.reserve(desired.size());
|
||||
for (int i = 0; i < desired.size(); ++i)
|
||||
desiredMap[desired[i].id] = i;
|
||||
|
||||
// Remove stale items (backwards)
|
||||
for (int i = model->rowCount() - 1; i >= 0; --i) {
|
||||
uint64_t id = model->item(i)->data(Qt::UserRole + 1).toULongLong();
|
||||
if (!desiredMap.contains(id))
|
||||
model->removeRow(i);
|
||||
}
|
||||
|
||||
// Update existing items
|
||||
QSet<uint64_t> existing;
|
||||
for (int i = 0; i < model->rowCount(); ++i) {
|
||||
auto* item = model->item(i);
|
||||
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||
existing.insert(id);
|
||||
auto dit = desiredMap.find(id);
|
||||
if (dit == desiredMap.end()) continue;
|
||||
const Entry& e = desired[*dit];
|
||||
|
||||
QString display = typeDisplayString(e.node, e.tree);
|
||||
if (item->text() != display)
|
||||
item->setText(display);
|
||||
item->setData(QVariant::fromValue(e.subPtr), Qt::UserRole);
|
||||
|
||||
// Refresh children only when count changed (avoids destroying expansion state)
|
||||
if (!e.isEnum) {
|
||||
QVector<int> members = e.tree->childrenOf(id);
|
||||
int visCount = 0;
|
||||
for (int mi : members)
|
||||
if (!isHexPad(e.tree->nodes[mi].kind)) ++visCount;
|
||||
if (item->rowCount() != visCount)
|
||||
buildStructChildren(item, e.tree, id, e.subPtr);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new items
|
||||
for (const auto& e : desired) {
|
||||
if (existing.contains(e.id)) continue;
|
||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||
}
|
||||
|
||||
if (model->horizontalHeaderItem(0) == nullptr)
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
}
|
||||
|
||||
// ── Custom delegate for rich workspace tree rendering ──
|
||||
|
||||
class WorkspaceDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
using QStyledItemDelegate::QStyledItemDelegate;
|
||||
|
||||
void setThemeColors(const Theme& t) {
|
||||
m_text = t.text;
|
||||
m_textDim = t.textDim;
|
||||
m_textMuted = t.textMuted;
|
||||
m_syntaxType = t.syntaxType;
|
||||
m_hover = t.hover;
|
||||
m_selected = t.selected;
|
||||
m_accent = t.borderFocused; // left accent bar
|
||||
m_bg = t.background;
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
QSize s = QStyledItemDelegate::sizeHint(option, index);
|
||||
int pad = index.parent().isValid() ? 6 : 10;
|
||||
s.setHeight(option.fontMetrics.height() + pad);
|
||||
return s;
|
||||
}
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const override {
|
||||
painter->save();
|
||||
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
opt.text.clear();
|
||||
opt.icon = QIcon(); // we draw icon manually
|
||||
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
|
||||
|
||||
// Custom background for selection/hover
|
||||
if (opt.state & QStyle::State_Selected) {
|
||||
painter->fillRect(opt.rect, m_selected);
|
||||
// Left accent bar
|
||||
painter->fillRect(QRect(opt.rect.x(), opt.rect.y(), 1, opt.rect.height()), m_accent);
|
||||
} else if (opt.state & QStyle::State_MouseOver) {
|
||||
painter->fillRect(opt.rect, m_hover);
|
||||
}
|
||||
|
||||
bool isChild = index.parent().isValid();
|
||||
QString fullText = index.data(Qt::DisplayRole).toString();
|
||||
QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
|
||||
|
||||
// Draw icon for top-level items
|
||||
if (!isChild) {
|
||||
QVariant iconVar = index.data(Qt::DecorationRole);
|
||||
if (iconVar.isValid()) {
|
||||
QIcon icon = iconVar.value<QIcon>();
|
||||
int iconSz = opt.fontMetrics.height();
|
||||
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
|
||||
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
|
||||
textRect.setLeft(textRect.left() + iconSz + 4);
|
||||
}
|
||||
}
|
||||
|
||||
painter->setFont(opt.font);
|
||||
|
||||
if (!isChild) {
|
||||
// Top-level: "StructName — 3"
|
||||
int dashPos = fullText.indexOf(QChar(0x2014));
|
||||
if (dashPos > 1) {
|
||||
QString name = fullText.left(dashPos - 1);
|
||||
QString meta = fullText.mid(dashPos - 1);
|
||||
|
||||
painter->setPen(m_text);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
||||
int nameW = opt.fontMetrics.horizontalAdvance(name);
|
||||
|
||||
QRect metaRect = textRect;
|
||||
metaRect.setLeft(textRect.left() + nameW);
|
||||
painter->setPen(m_textMuted);
|
||||
painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta);
|
||||
} else {
|
||||
painter->setPen(m_text);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||
}
|
||||
} else {
|
||||
// Child: "TypeName fieldName"
|
||||
int spacePos = fullText.indexOf(' ');
|
||||
if (spacePos > 0) {
|
||||
QString typeName = fullText.left(spacePos);
|
||||
QString fieldName = fullText.mid(spacePos);
|
||||
|
||||
painter->setPen(m_syntaxType);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, typeName);
|
||||
int typeW = opt.fontMetrics.horizontalAdvance(typeName);
|
||||
|
||||
QRect fieldRect = textRect;
|
||||
fieldRect.setLeft(textRect.left() + typeW);
|
||||
painter->setPen(m_textDim);
|
||||
painter->drawText(fieldRect, Qt::AlignLeft | Qt::AlignVCenter, fieldName);
|
||||
} else {
|
||||
painter->setPen(m_textDim);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||
}
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
private:
|
||||
QColor m_text, m_textDim, m_textMuted, m_syntaxType;
|
||||
QColor m_hover, m_selected, m_accent, m_bg;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -65,6 +65,7 @@ private:
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void benchCompose();
|
||||
void benchComposeLarge();
|
||||
void benchApplyDocument();
|
||||
void benchHoverHighlight();
|
||||
void benchSelectionOverlay();
|
||||
@@ -112,6 +113,36 @@ void BenchLargeClass::benchCompose()
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchComposeLarge()
|
||||
{
|
||||
// Build a 2000-field tree to stress-test compose at scale
|
||||
NodeTree bigTree = buildLargeTree(2000);
|
||||
QByteArray buf(0x40000, '\0');
|
||||
for (int i = 0; i < buf.size(); ++i) buf[i] = (char)(i & 0xFF);
|
||||
BufferProvider bigProv(buf, QStringLiteral("bench_large"));
|
||||
|
||||
// Warmup
|
||||
{ ComposeResult w = rcx::compose(bigTree, bigProv); Q_UNUSED(w); }
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
ComposeResult r = rcx::compose(bigTree, bigProv);
|
||||
Q_UNUSED(r);
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Compose Benchmark (2000 fields) ===";
|
||||
qDebug() << " Tree:" << bigTree.nodes.size() << "nodes";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
|
||||
QVERIFY(elapsed > 0);
|
||||
}
|
||||
|
||||
void BenchLargeClass::benchApplyDocument()
|
||||
{
|
||||
RcxEditor editor;
|
||||
|
||||
282
tests/bench_project.cpp
Normal file
282
tests/bench_project.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* bench_project — benchmark project lifecycle operations:
|
||||
* - New class creation
|
||||
* - Loading large .rcx files (WinSDK, Vergilius)
|
||||
* - Workspace model building
|
||||
* - Workspace search filtering
|
||||
* - JSON parsing vs model building breakdown
|
||||
*/
|
||||
#include <QtTest/QtTest>
|
||||
#include <QElapsedTimer>
|
||||
#include <QJsonDocument>
|
||||
#include <QStandardItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include "core.h"
|
||||
#include "controller.h"
|
||||
#include "workspace_model.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class BenchProject : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void benchNewClass();
|
||||
void benchLoadVergilius();
|
||||
void benchLoadWinSDK();
|
||||
void benchJsonParse();
|
||||
void benchNodeTreeFromJson();
|
||||
void benchBuildWorkspaceModel();
|
||||
void benchWorkspaceSearch();
|
||||
};
|
||||
|
||||
static QString findExample(const QString& name) {
|
||||
// Try relative to executable, then common build layout
|
||||
QStringList candidates = {
|
||||
QCoreApplication::applicationDirPath() + "/examples/" + name,
|
||||
QCoreApplication::applicationDirPath() + "/../src/examples/" + name,
|
||||
QStringLiteral("src/examples/") + name,
|
||||
QStringLiteral("../src/examples/") + name,
|
||||
};
|
||||
for (const auto& c : candidates)
|
||||
if (QFileInfo::exists(c)) return c;
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── New class (just the core operations, no UI) ──
|
||||
|
||||
void BenchProject::benchNewClass()
|
||||
{
|
||||
const int ITERS = 1000;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("NewClass");
|
||||
root.structTypeName = QStringLiteral("NewClass");
|
||||
root.classKeyword = QStringLiteral("class");
|
||||
tree.addNode(root);
|
||||
// Add 8 hex64 padding fields (what buildEmptyStruct does)
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Hex64;
|
||||
pad.name = QString();
|
||||
pad.parentId = rootId;
|
||||
pad.offset = j * 8;
|
||||
tree.addNode(pad);
|
||||
}
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== New Class (core tree build) ===";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-new:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Load .rcx files ──
|
||||
|
||||
static bool loadRcx(const QString& path, NodeTree& tree) {
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) return false;
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
tree = NodeTree::fromJson(jdoc.object());
|
||||
return !tree.nodes.isEmpty();
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadVergilius()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) { QSKIP("Vergilius_25H2.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load Vergilius_25H2.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchLoadWinSDK()
|
||||
{
|
||||
QString path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("WinSDK.rcx not found"); return; }
|
||||
|
||||
const int ITERS = 5;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
NodeTree tree;
|
||||
QVERIFY(loadRcx(path, tree));
|
||||
if (i == 0)
|
||||
qDebug() << " Nodes:" << tree.nodes.size();
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== Load WinSDK.rcx ===";
|
||||
qDebug() << " File:" << QFileInfo(path).size() / 1024 << "KB";
|
||||
qDebug() << " Iterations:" << ITERS;
|
||||
qDebug() << " Total:" << elapsed << "ms";
|
||||
qDebug() << " Per-load:" << (double)elapsed / ITERS << "ms";
|
||||
}
|
||||
|
||||
// ── Breakdown: JSON parse vs NodeTree build ──
|
||||
|
||||
void BenchProject::benchJsonParse()
|
||||
{
|
||||
QString path = findExample("Vergilius_25H2.rcx");
|
||||
if (path.isEmpty()) path = findExample("WinSDK.rcx");
|
||||
if (path.isEmpty()) { QSKIP("No large .rcx found"); return; }
|
||||
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const int ITERS = 5;
|
||||
|
||||
// Phase 1: raw JSON parse
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QJsonDocument jdoc;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
jdoc = QJsonDocument::fromJson(data);
|
||||
qint64 jsonMs = timer.elapsed();
|
||||
|
||||
// Phase 2: NodeTree::fromJson
|
||||
QJsonObject root = jdoc.object();
|
||||
timer.start();
|
||||
NodeTree tree;
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
tree = NodeTree::fromJson(root);
|
||||
qint64 treeMs = timer.elapsed();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== JSON Parse Breakdown ===" << QFileInfo(path).fileName();
|
||||
qDebug() << " File:" << data.size() / 1024 << "KB," << tree.nodes.size() << "nodes";
|
||||
qDebug() << " JSON parse:" << (double)jsonMs / ITERS << "ms/iter";
|
||||
qDebug() << " NodeTree build:" << (double)treeMs / ITERS << "ms/iter";
|
||||
qDebug() << " Total per-load:" << (double)(jsonMs + treeMs) / ITERS << "ms";
|
||||
}
|
||||
|
||||
void BenchProject::benchNodeTreeFromJson()
|
||||
{
|
||||
// Already covered by benchJsonParse breakdown
|
||||
QVERIFY(true);
|
||||
}
|
||||
|
||||
// ── Workspace model building ──
|
||||
|
||||
void BenchProject::benchBuildWorkspaceModel()
|
||||
{
|
||||
// Load both large examples if available
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
// Build TabInfo array
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
const int ITERS = 20;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
buildProjectExplorer(&model, tabs);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
// Count items
|
||||
int topLevel = model.rowCount();
|
||||
int totalChildren = 0;
|
||||
for (int i = 0; i < topLevel; ++i)
|
||||
totalChildren += model.item(i)->rowCount();
|
||||
|
||||
int totalNodes = 0;
|
||||
for (const auto& t : trees) totalNodes += t.nodes.size();
|
||||
fprintf(stderr, "\n=== Build Workspace Model ===\n");
|
||||
fprintf(stderr, " Trees: %d total nodes: %d\n", (int)trees.size(), totalNodes);
|
||||
fprintf(stderr, " Top-level items: %d child items: %d\n", topLevel, totalChildren);
|
||||
fprintf(stderr, " Iterations: %d\n", ITERS);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-build: %.1f ms\n", (double)elapsed / ITERS);
|
||||
}
|
||||
|
||||
// ── Workspace search filtering ──
|
||||
|
||||
void BenchProject::benchWorkspaceSearch()
|
||||
{
|
||||
QVector<NodeTree> trees;
|
||||
for (const auto& name : {QStringLiteral("Vergilius_25H2.rcx"), QStringLiteral("WinSDK.rcx")}) {
|
||||
QString path = findExample(name);
|
||||
if (path.isEmpty()) continue;
|
||||
NodeTree t;
|
||||
if (loadRcx(path, t)) trees.append(std::move(t));
|
||||
}
|
||||
if (trees.isEmpty()) { QSKIP("No .rcx examples found"); return; }
|
||||
|
||||
QVector<TabInfo> tabs;
|
||||
for (const auto& t : trees)
|
||||
tabs.append({ &t, QStringLiteral("test"), nullptr });
|
||||
|
||||
QStandardItemModel model;
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QSortFilterProxyModel proxy;
|
||||
proxy.setSourceModel(&model);
|
||||
proxy.setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy.setRecursiveFilteringEnabled(true);
|
||||
|
||||
const QStringList queries = {
|
||||
"EPROCESS", "KTHREAD", "LIST_ENTRY", "HAL", "DMA",
|
||||
"xyz_no_match", "a", "Dispatch"
|
||||
};
|
||||
|
||||
const int ITERS = 50;
|
||||
QElapsedTimer timer;
|
||||
|
||||
timer.start();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
for (const auto& q : queries)
|
||||
proxy.setFilterFixedString(q);
|
||||
proxy.setFilterFixedString(QString()); // clear
|
||||
}
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
int totalOps = ITERS * (queries.size() + 1);
|
||||
fprintf(stderr, "\n=== Workspace Search Filter ===\n");
|
||||
fprintf(stderr, " Model rows: %d queries: %d\n", model.rowCount(), (int)queries.size());
|
||||
fprintf(stderr, " Iterations: %d total filter ops: %d\n", ITERS, totalOps);
|
||||
fprintf(stderr, " Total: %lld ms\n", (long long)elapsed);
|
||||
fprintf(stderr, " Per-filter: %.2f ms\n", (double)elapsed / totalOps);
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchProject)
|
||||
#include "bench_project.moc"
|
||||
@@ -234,6 +234,7 @@ private slots:
|
||||
child.name = "Child";
|
||||
child.parentId = rootId;
|
||||
child.offset = 0;
|
||||
child.collapsed = false;
|
||||
int ci = tree.addNode(child);
|
||||
uint64_t childId = tree.nodes[ci].id;
|
||||
|
||||
@@ -281,6 +282,7 @@ private slots:
|
||||
inner.name = "Inner";
|
||||
inner.parentId = rootId;
|
||||
inner.offset = 4;
|
||||
inner.collapsed = false;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
|
||||
@@ -354,6 +356,7 @@ private slots:
|
||||
tmpl.name = "VTable";
|
||||
tmpl.parentId = 0;
|
||||
tmpl.offset = 200; // far away so standalone rendering uses offset 200
|
||||
tmpl.collapsed = false;
|
||||
int ti = tree.addNode(tmpl);
|
||||
uint64_t tmplId = tree.nodes[ti].id;
|
||||
|
||||
@@ -378,6 +381,7 @@ private slots:
|
||||
ptr.parentId = mainId;
|
||||
ptr.offset = 4;
|
||||
ptr.refId = tmplId;
|
||||
ptr.collapsed = false;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// Provider: pointer at offset 4 points to address 100
|
||||
@@ -434,6 +438,7 @@ private slots:
|
||||
tmpl.name = "Target";
|
||||
tmpl.parentId = 0;
|
||||
tmpl.offset = 200;
|
||||
tmpl.collapsed = false;
|
||||
int ti = tree.addNode(tmpl);
|
||||
uint64_t tmplId = tree.nodes[ti].id;
|
||||
|
||||
@@ -450,6 +455,7 @@ private slots:
|
||||
ptr.parentId = mainId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = tmplId;
|
||||
ptr.collapsed = false;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// All zeros = null pointer
|
||||
@@ -493,6 +499,7 @@ private slots:
|
||||
tmpl.name = "Target";
|
||||
tmpl.parentId = 0;
|
||||
tmpl.offset = 200;
|
||||
tmpl.collapsed = false; // standalone rendering shows children
|
||||
int ti = tree.addNode(tmpl);
|
||||
uint64_t tmplId = tree.nodes[ti].id;
|
||||
|
||||
@@ -509,7 +516,7 @@ private slots:
|
||||
ptr.parentId = mainId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = tmplId;
|
||||
ptr.collapsed = true; // collapsed
|
||||
ptr.collapsed = true; // collapsed — this is the test condition
|
||||
tree.addNode(ptr);
|
||||
|
||||
// Non-null pointer
|
||||
@@ -548,6 +555,7 @@ private slots:
|
||||
tmpl.name = "Recursive";
|
||||
tmpl.parentId = 0;
|
||||
tmpl.offset = 200;
|
||||
tmpl.collapsed = false;
|
||||
int ti = tree.addNode(tmpl);
|
||||
uint64_t tmplId = tree.nodes[ti].id;
|
||||
|
||||
@@ -565,6 +573,7 @@ private slots:
|
||||
backPtr.parentId = tmplId;
|
||||
backPtr.offset = 4;
|
||||
backPtr.refId = tmplId; // points back to same struct
|
||||
backPtr.collapsed = false;
|
||||
tree.addNode(backPtr);
|
||||
|
||||
// Pointer in Main → Recursive
|
||||
@@ -574,6 +583,7 @@ private slots:
|
||||
ptr.parentId = mainId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = tmplId;
|
||||
ptr.collapsed = false;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// Provider: main ptr at offset 0 points to 100
|
||||
@@ -696,6 +706,7 @@ private slots:
|
||||
arr.offset = 0;
|
||||
arr.elementKind = NodeKind::Int32;
|
||||
arr.arrayLen = 10;
|
||||
arr.collapsed = false;
|
||||
tree.addNode(arr);
|
||||
|
||||
NullProvider prov;
|
||||
@@ -847,6 +858,7 @@ private slots:
|
||||
arr.offset = 0;
|
||||
arr.elementKind = NodeKind::Int32;
|
||||
arr.arrayLen = 2;
|
||||
arr.collapsed = false;
|
||||
int ai = tree.addNode(arr);
|
||||
uint64_t arrId = tree.nodes[ai].id;
|
||||
|
||||
@@ -856,6 +868,7 @@ private slots:
|
||||
elem0.name = "Item";
|
||||
elem0.parentId = arrId;
|
||||
elem0.offset = 0;
|
||||
elem0.collapsed = false;
|
||||
int e0i = tree.addNode(elem0);
|
||||
uint64_t elem0Id = tree.nodes[e0i].id;
|
||||
|
||||
@@ -871,6 +884,7 @@ private slots:
|
||||
elem1.name = "Item";
|
||||
elem1.parentId = arrId;
|
||||
elem1.offset = 4;
|
||||
elem1.collapsed = false;
|
||||
int e1i = tree.addNode(elem1);
|
||||
uint64_t elem1Id = tree.nodes[e1i].id;
|
||||
|
||||
@@ -1035,6 +1049,7 @@ private slots:
|
||||
arr.offset = 0;
|
||||
arr.elementKind = NodeKind::UInt32;
|
||||
arr.arrayLen = 4;
|
||||
arr.collapsed = false;
|
||||
tree.addNode(arr);
|
||||
|
||||
// Buffer with known values: 0x11, 0x22, 0x33, 0x44
|
||||
@@ -1140,6 +1155,7 @@ private slots:
|
||||
arr.offset = 0;
|
||||
arr.elementKind = NodeKind::Struct;
|
||||
arr.arrayLen = 1;
|
||||
arr.collapsed = false;
|
||||
int ai = tree.addNode(arr);
|
||||
uint64_t arrId = tree.nodes[ai].id;
|
||||
|
||||
@@ -1149,6 +1165,7 @@ private slots:
|
||||
elem.name = "Item";
|
||||
elem.parentId = arrId;
|
||||
elem.offset = 0;
|
||||
elem.collapsed = false;
|
||||
int ei = tree.addNode(elem);
|
||||
uint64_t elemId = tree.nodes[ei].id;
|
||||
|
||||
@@ -1481,6 +1498,7 @@ private slots:
|
||||
structC.structTypeName = "InnerData";
|
||||
structC.parentId = 0;
|
||||
structC.offset = 300;
|
||||
structC.collapsed = false;
|
||||
int ci = tree.addNode(structC);
|
||||
uint64_t structCId = tree.nodes[ci].id;
|
||||
|
||||
@@ -1498,6 +1516,7 @@ private slots:
|
||||
structB.structTypeName = "Wrapper";
|
||||
structB.parentId = 0;
|
||||
structB.offset = 200;
|
||||
structB.collapsed = false;
|
||||
int bi = tree.addNode(structB);
|
||||
uint64_t structBId = tree.nodes[bi].id;
|
||||
|
||||
@@ -1514,6 +1533,7 @@ private slots:
|
||||
bptr.parentId = structBId;
|
||||
bptr.offset = 4;
|
||||
bptr.refId = structCId; // points to InnerData
|
||||
bptr.collapsed = false;
|
||||
tree.addNode(bptr);
|
||||
|
||||
// Root's pointer to StructB
|
||||
@@ -1523,6 +1543,7 @@ private slots:
|
||||
rptr.parentId = rootId;
|
||||
rptr.offset = 0;
|
||||
rptr.refId = structBId;
|
||||
rptr.collapsed = false;
|
||||
tree.addNode(rptr);
|
||||
|
||||
// Provider: rptr at 0 → addr 100, bptr at 100+4=104 → addr 150
|
||||
@@ -1591,6 +1612,7 @@ private slots:
|
||||
structB.name = "StructB";
|
||||
structB.parentId = 0;
|
||||
structB.offset = 200;
|
||||
structB.collapsed = false;
|
||||
int bi = tree.addNode(structB);
|
||||
uint64_t structBId = tree.nodes[bi].id;
|
||||
|
||||
@@ -1608,6 +1630,7 @@ private slots:
|
||||
ptrToB.parentId = mainId;
|
||||
ptrToB.offset = 4;
|
||||
ptrToB.refId = structBId;
|
||||
ptrToB.collapsed = false;
|
||||
tree.addNode(ptrToB);
|
||||
|
||||
// StructB → Main pointer (creates cycle!)
|
||||
@@ -1617,6 +1640,7 @@ private slots:
|
||||
ptrToMain.parentId = structBId;
|
||||
ptrToMain.offset = 4;
|
||||
ptrToMain.refId = mainId;
|
||||
ptrToMain.collapsed = false;
|
||||
tree.addNode(ptrToMain);
|
||||
|
||||
// Provider: Main.to_b at offset 4 → addr 100
|
||||
@@ -2009,6 +2033,7 @@ private slots:
|
||||
u.name = "u1";
|
||||
u.parentId = rootId;
|
||||
u.offset = 0;
|
||||
u.collapsed = false;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
@@ -2655,6 +2680,7 @@ private slots:
|
||||
inner.kind = NodeKind::Struct;
|
||||
inner.name = "NewClass";
|
||||
inner.parentId = 0;
|
||||
inner.collapsed = false;
|
||||
inner.offset = 200;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
@@ -2688,6 +2714,7 @@ private slots:
|
||||
ptr.parentId = rootId;
|
||||
ptr.offset = 8;
|
||||
ptr.refId = innerId;
|
||||
ptr.collapsed = false;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// Last child: hex64 at depth 1
|
||||
|
||||
@@ -214,6 +214,7 @@ private slots:
|
||||
vptr.parentId = rootId;
|
||||
vptr.offset = 0;
|
||||
vptr.refId = vtId;
|
||||
vptr.collapsed = false;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose the tree
|
||||
@@ -408,6 +409,7 @@ private slots:
|
||||
|
||||
Node vptr; vptr.kind = NodeKind::Pointer64; vptr.name = "__vptr";
|
||||
vptr.parentId = rootId; vptr.offset = 0; vptr.refId = vtId;
|
||||
vptr.collapsed = false;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose with the snapshot (like production: compose uses snapshot)
|
||||
|
||||
@@ -498,9 +498,9 @@ void TestImportSource::computedOffsets() {
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 4);
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0); // uint8_t at 0
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 1); // uint16_t at 1
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 3); // uint32_t at 3
|
||||
QCOMPARE(tree.nodes[kids[3]].offset, 7); // uint64_t at 7
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 2); // uint16_t at 2 (aligned)
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 4); // uint32_t at 4 (aligned)
|
||||
QCOMPARE(tree.nodes[kids[3]].offset, 8); // uint64_t at 8 (aligned)
|
||||
}
|
||||
|
||||
void TestImportSource::mixedOffsetsAutoDetect() {
|
||||
@@ -805,7 +805,7 @@ void TestImportSource::bitfieldSkipped() {
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitWidth, (uint8_t)12);
|
||||
QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitOffset, (uint8_t)4);
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 6);
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 8); // aligned to uint32_t boundary
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||
|
||||
@@ -517,16 +517,11 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Single "Project" root
|
||||
// Flat model: Player at root (has 2 non-hex members → lazy placeholder)
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->text(), QString("Project"));
|
||||
|
||||
// 1 type directly under Project: Player (no member fields)
|
||||
QCOMPARE(project->rowCount(), 1);
|
||||
QVERIFY(project->child(0)->text().contains("Player"));
|
||||
QVERIFY(project->child(0)->text().contains("struct"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QVERIFY(model.item(0)->text().contains("Player"));
|
||||
QVERIFY(model.item(0)->text().contains("struct"));
|
||||
QVERIFY(model.item(0)->rowCount() > 0); // children populated directly
|
||||
}
|
||||
|
||||
void testWorkspace_twoRootTree() {
|
||||
@@ -535,15 +530,10 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
|
||||
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
||||
QCOMPARE(project->rowCount(), 2);
|
||||
QVERIFY(project->child(0)->text().contains("Alpha"));
|
||||
QVERIFY(project->child(1)->text().contains("Bravo"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
// Flat model: 2 types at root
|
||||
QCOMPARE(model.rowCount(), 2);
|
||||
QVERIFY(model.item(0)->text().contains("Alpha"));
|
||||
QVERIFY(model.item(1)->text().contains("Bravo"));
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_rootCount() {
|
||||
@@ -552,25 +542,19 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||
QCOMPARE(model.rowCount(), 3); // Ball, Cat, Pet
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_sorted() {
|
||||
void testWorkspace_richTree_insertionOrder() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Sorted alphabetically: Ball, Cat, Pet
|
||||
QVERIFY(project->child(0)->text().contains("Ball"));
|
||||
QVERIFY(project->child(1)->text().contains("Cat"));
|
||||
QVERIFY(project->child(2)->text().contains("Pet"));
|
||||
// No member fields under type nodes
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
QCOMPARE(project->child(2)->rowCount(), 0);
|
||||
// Types at root in insertion order: Pet, Cat, Ball
|
||||
QVERIFY(model.item(0)->text().contains("Pet"));
|
||||
QVERIFY(model.item(1)->text().contains("Cat"));
|
||||
QVERIFY(model.item(2)->text().contains("Ball"));
|
||||
}
|
||||
|
||||
void testWorkspace_emptyTree() {
|
||||
@@ -579,10 +563,8 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Still has the "Project" root, just no children
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.item(0)->text(), QString("Project"));
|
||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
||||
// Flat model: no types means no rows
|
||||
QCOMPARE(model.rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_structIdRole() {
|
||||
@@ -591,15 +573,11 @@ private slots:
|
||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Project root has kGroupSentinel
|
||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||
|
||||
// Player type item should have structId
|
||||
QStandardItem* player = project->child(0);
|
||||
// Flat model: first item is the Player type with its structId
|
||||
QVERIFY(model.rowCount() > 0);
|
||||
QStandardItem* player = model.item(0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
@@ -360,17 +360,45 @@ private slots:
|
||||
}
|
||||
|
||||
void testComposeNoStaticFieldsWhenCollapsed() {
|
||||
TestTree t;
|
||||
t.addField("x", NodeKind::Float, 0);
|
||||
t.addStaticField("h", "base");
|
||||
// Collapse the root struct
|
||||
t.tree.nodes[0].collapsed = true;
|
||||
// Use a non-root struct to test collapsed behavior
|
||||
// (root structs are always expanded via isRootHeader)
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node child;
|
||||
child.kind = NodeKind::Struct;
|
||||
child.name = "Child";
|
||||
child.parentId = rootId;
|
||||
child.offset = 0;
|
||||
child.collapsed = true; // collapsed child struct
|
||||
int ci = tree.addNode(child);
|
||||
uint64_t childId = tree.nodes[ci].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::Float;
|
||||
f.name = "x";
|
||||
f.parentId = childId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
Node sf;
|
||||
sf.kind = NodeKind::Hex64;
|
||||
sf.name = "h";
|
||||
sf.parentId = childId;
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base");
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(t.tree, prov);
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// When collapsed, no static field lines should appear
|
||||
QStringList lines = result.text.split('\n');
|
||||
for (const auto& lm : result.meta)
|
||||
QVERIFY2(!lm.isStaticLine,
|
||||
"Static field line should not appear when struct is collapsed");
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
using namespace rcx;
|
||||
|
||||
static const char* CDB_PATH = "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\cdb.exe";
|
||||
static const int DBG_PORT = 5055;
|
||||
static const int DBG_PORT = 5056;
|
||||
|
||||
class TestWinDbgProvider : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -132,7 +132,7 @@ private slots:
|
||||
|
||||
void initTestCase()
|
||||
{
|
||||
m_connString = QString("tcp:Port=%1,Server=localhost").arg(DBG_PORT);
|
||||
m_connString = QString("tcp:Port=%1,Server=127.0.0.1").arg(DBG_PORT);
|
||||
|
||||
// If a debug server is already listening (e.g. WinDbg with .server),
|
||||
// skip launching our own cdb.exe.
|
||||
@@ -207,7 +207,7 @@ private slots:
|
||||
void plugin_canHandle_tcp()
|
||||
{
|
||||
WinDbgMemoryPlugin plugin;
|
||||
QVERIFY(plugin.canHandle("tcp:Port=5055,Server=localhost"));
|
||||
QVERIFY(plugin.canHandle("tcp:Port=5056,Server=localhost"));
|
||||
QVERIFY(plugin.canHandle("TCP:Port=1234,Server=10.0.0.1"));
|
||||
}
|
||||
|
||||
@@ -608,7 +608,7 @@ private slots:
|
||||
// ── Kernel/dump session tests ──
|
||||
// Set WINDBG_KERNEL_CONN to a target string:
|
||||
// "dump:F:/path/to/file.dmp" — open dump directly
|
||||
// "tcp:Port=5055,Server=localhost" — connect to debug server
|
||||
// "tcp:Port=5056,Server=localhost" — connect to debug server
|
||||
// Set WINDBG_KERNEL_ADDR to a readable hex address (e.g. kernel base).
|
||||
|
||||
static QString kernelTarget()
|
||||
|
||||
@@ -341,6 +341,19 @@ def parse_field_line(line, offset, parent_id, ids, struct_registry):
|
||||
line = re.sub(r'\bvolatile\b', '', line).strip()
|
||||
line = re.sub(r'\s+', ' ', line)
|
||||
|
||||
# Check for function pointer: RETURN_TYPE (*NAME)(PARAMS)
|
||||
fnptr_m = re.search(r'\(\*\s*(\w+)\s*\)', line)
|
||||
if fnptr_m:
|
||||
field_name = fnptr_m.group(1)
|
||||
node_id = ids.alloc()
|
||||
return {
|
||||
'id': str(node_id),
|
||||
'kind': 'FuncPtr64',
|
||||
'name': field_name,
|
||||
'offset': offset,
|
||||
'parentId': str(parent_id),
|
||||
}
|
||||
|
||||
# Check for struct/union keyword prefix
|
||||
keyword = None
|
||||
m = re.match(r'^(struct|union|enum)\s+(.+)', line)
|
||||
|
||||
Reference in New Issue
Block a user