mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
25 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596f410b96 | ||
|
|
f0fc85f60f | ||
|
|
70c7404556 | ||
|
|
f27459c21b | ||
|
|
a5abcbeea6 | ||
|
|
7071402319 | ||
|
|
0dc390ed86 | ||
|
|
188c27c6e2 | ||
|
|
81f1e4319f | ||
|
|
3ab6affa5e | ||
|
|
35b3cd9ac1 | ||
|
|
e5938f7e82 | ||
|
|
03c49d19dd | ||
|
|
b7eebedf50 | ||
|
|
9ff456a8d6 | ||
|
|
580f285edd | ||
|
|
d23a6c7656 | ||
|
|
25d8de95b7 | ||
|
|
955db3813a | ||
|
|
f4f203e0f0 | ||
|
|
1d3f1a672a | ||
|
|
da29206bdb | ||
|
|
4986893fca | ||
|
|
17a1fb032e | ||
|
|
8d92957837 |
@@ -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)
|
||||
@@ -109,6 +135,8 @@ add_executable(Reclass
|
||||
src/scannerpanel.h
|
||||
src/scannerpanel.cpp
|
||||
src/mainwindow.h
|
||||
src/startpage.h
|
||||
src/dock_tab_buttons.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
@@ -149,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)
|
||||
@@ -162,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)
|
||||
@@ -176,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)
|
||||
@@ -498,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">
|
||||
@@ -140,10 +142,12 @@
|
||||
<ClInclude Include="..\src\addressparser.h" />
|
||||
<ClInclude Include="..\src\core.h" />
|
||||
<ClInclude Include="..\src\disasm.h" />
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h" />
|
||||
<ClInclude Include="..\src\generator.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\pluginmanager.h" />
|
||||
<ClInclude Include="..\src\providerregistry.h" />
|
||||
<QtMoc Include="..\src\startpage.h" />
|
||||
<ClInclude Include="..\src\workspace_model.h" />
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_pdb.h" />
|
||||
@@ -163,7 +167,12 @@
|
||||
<ClCompile Include="..\src\editor.cpp" />
|
||||
<ClCompile Include="..\src\format.cpp" />
|
||||
<ClCompile Include="..\src\generator.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp" />
|
||||
<ClCompile Include="..\src\pluginmanager.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
<QtMoc Include="..\src\themes\thememanager.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\startpage.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h">
|
||||
@@ -165,9 +171,6 @@
|
||||
<ClCompile Include="..\src\generator.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -219,5 +222,8 @@
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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) {
|
||||
|
||||
36
src/dock_tab_buttons.h
Normal file
36
src/dock_tab_buttons.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
|
||||
// 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* closeBtn;
|
||||
|
||||
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* hl = new QHBoxLayout(this);
|
||||
hl->setContentsMargins(0, 0, 0, 0);
|
||||
hl->setSpacing(0);
|
||||
|
||||
closeBtn = new QToolButton(this);
|
||||
closeBtn->setAutoRaise(true);
|
||||
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||
closeBtn->setFixedSize(16, 16);
|
||||
closeBtn->setToolTip("Close tab");
|
||||
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
|
||||
closeBtn->setIconSize(QSize(12, 12));
|
||||
hl->addWidget(closeBtn);
|
||||
}
|
||||
|
||||
void applyTheme(const QColor& hover) {
|
||||
QString style = QStringLiteral(
|
||||
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||
closeBtn->setStyleSheet(style);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
{
|
||||
"baseAddress": "0",
|
||||
"nextId": "20",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1",
|
||||
"kind": "Struct",
|
||||
"name": "player",
|
||||
"structTypeName": "PlayerEntity",
|
||||
"classKeyword": "class",
|
||||
"parentId": "0",
|
||||
"offset": 0,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"kind": "Pointer64",
|
||||
"name": "__vptr",
|
||||
"parentId": "1",
|
||||
"offset": 0,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"kind": "Int32",
|
||||
"name": "health",
|
||||
"parentId": "1",
|
||||
"offset": 8,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"kind": "Int32",
|
||||
"name": "armor",
|
||||
"parentId": "1",
|
||||
"offset": 12,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"kind": "Float",
|
||||
"name": "pos_x",
|
||||
"parentId": "1",
|
||||
"offset": 16,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"kind": "Float",
|
||||
"name": "pos_y",
|
||||
"parentId": "1",
|
||||
"offset": 20,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"kind": "Float",
|
||||
"name": "pos_z",
|
||||
"parentId": "1",
|
||||
"offset": 24,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"kind": "Hex32",
|
||||
"name": "pad_1C",
|
||||
"parentId": "1",
|
||||
"offset": 28,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"kind": "Pointer64",
|
||||
"name": "name",
|
||||
"parentId": "1",
|
||||
"offset": 32,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64,
|
||||
"ptrDepth": 1
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"kind": "UInt64",
|
||||
"name": "flags",
|
||||
"parentId": "1",
|
||||
"offset": 40,
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"kind": "Hex64",
|
||||
"name": "static_field",
|
||||
"parentId": "1",
|
||||
"offset": 0,
|
||||
"isStatic": true,
|
||||
"offsetExpr": "base + pos_x",
|
||||
"collapsed": false,
|
||||
"refId": "0",
|
||||
"elementKind": "UInt8",
|
||||
"arrayLen": 1,
|
||||
"strLen": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
244419
src/examples/WinSDK.rcx
Normal file
244419
src/examples/WinSDK.rcx
Normal file
File diff suppressed because it is too large
Load Diff
42817
src/examples/windows-x86_64.h
Normal file
42817
src/examples/windows-x86_64.h
Normal file
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) {
|
||||
|
||||
502
src/main.cpp
502
src/main.cpp
@@ -480,64 +480,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// ── Dock tab button widget (pin + close) ──
|
||||
// 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);
|
||||
closeBtn->setFixedSize(16, 16);
|
||||
closeBtn->setToolTip("Close tab");
|
||||
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");
|
||||
}
|
||||
};
|
||||
#include "dock_tab_buttons.h"
|
||||
|
||||
static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
QPalette pal;
|
||||
@@ -566,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 {
|
||||
@@ -631,6 +586,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
overlay->raise();
|
||||
overlay->show();
|
||||
|
||||
// Central placeholder — will be replaced by start page after construction
|
||||
m_centralPlaceholder = new QWidget(this);
|
||||
m_centralPlaceholder->setFixedSize(0, 0);
|
||||
m_centralPlaceholder->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||
@@ -669,6 +625,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
||||
this, &MainWindow::applyTheme);
|
||||
|
||||
// Apply theme once at startup (the signal only fires on change, not initial load)
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
|
||||
// Load plugins
|
||||
m_pluginManager.LoadPlugins();
|
||||
|
||||
@@ -859,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)
|
||||
@@ -1271,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
|
||||
@@ -1494,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);
|
||||
@@ -1585,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());
|
||||
@@ -1597,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));
|
||||
@@ -1622,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
|
||||
@@ -1634,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()) {
|
||||
@@ -1666,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,
|
||||
@@ -1720,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();
|
||||
@@ -1737,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();
|
||||
@@ -1787,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
|
||||
@@ -1827,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) {
|
||||
@@ -1843,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;
|
||||
|
||||
@@ -1869,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)
|
||||
@@ -1912,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)
|
||||
@@ -1971,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";
|
||||
@@ -2345,19 +2327,25 @@ 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);
|
||||
|
||||
// Update border overlay color
|
||||
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||
|
||||
// Style doc dock tab bars and remove dock borders
|
||||
// Style doc dock tab bars and remove dock borders.
|
||||
// QWidget default colors are required because having ANY stylesheet on QMainWindow
|
||||
// switches children from palette-based to CSS-based rendering.
|
||||
setStyleSheet(QStringLiteral(
|
||||
"QMainWindow::separator { width: 1px; height: 1px; background: transparent; }"
|
||||
"QDockWidget { border: none; }"
|
||||
"QDockWidget > QWidget { border: none; }")
|
||||
.arg(theme.border.name()));
|
||||
"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),
|
||||
@@ -2365,6 +2353,7 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
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
|
||||
@@ -2393,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)
|
||||
@@ -2428,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
|
||||
@@ -2461,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)
|
||||
@@ -2528,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);
|
||||
@@ -2630,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);
|
||||
@@ -2656,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2687,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 {
|
||||
@@ -2743,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);
|
||||
@@ -3007,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));
|
||||
@@ -3061,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")
|
||||
@@ -3222,10 +3236,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
||||
if (filePath.isEmpty()) {
|
||||
filePath = QFileDialog::getOpenFileName(this,
|
||||
"Open Definition", {},
|
||||
"All Supported (*.rcx *.json *.reclass *.MemeCls *.xml)"
|
||||
";;Reclass (*.rcx)"
|
||||
";;JSON (*.json)"
|
||||
";;ReClass XML (*.reclass *.MemeCls *.xml)"
|
||||
"Reclass (*.rcx)"
|
||||
";;All (*)");
|
||||
if (filePath.isEmpty()) return nullptr;
|
||||
}
|
||||
@@ -3236,8 +3247,7 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
||||
QFile probe(filePath);
|
||||
if (probe.open(QIODevice::ReadOnly)) {
|
||||
QByteArray head = probe.read(64);
|
||||
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass")
|
||||
|| head.trimmed().startsWith("<MemeCls");
|
||||
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3256,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;
|
||||
@@ -3282,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);
|
||||
@@ -3312,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3338,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();
|
||||
@@ -3357,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);
|
||||
|
||||
@@ -3376,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);
|
||||
@@ -3384,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);
|
||||
|
||||
@@ -3408,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);
|
||||
@@ -3432,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");
|
||||
@@ -3450,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);
|
||||
@@ -3554,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*>());
|
||||
@@ -3759,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) {
|
||||
@@ -4007,6 +4098,67 @@ void MainWindow::updateBorderColor(const QColor& color) {
|
||||
m_borderOverlay->update();
|
||||
}
|
||||
|
||||
void MainWindow::showStartPage() {
|
||||
if (m_startPage) return;
|
||||
|
||||
m_startPage = new StartPageWidget(this);
|
||||
m_startPage->applyTheme(ThemeManager::instance().current());
|
||||
|
||||
// Size the popup to ~90% of the main window
|
||||
QSize sz(qBound(900, int(width() * 0.9), width() - 20),
|
||||
qBound(560, int(height() * 0.85), height() - 20));
|
||||
m_startPage->setFixedSize(sz);
|
||||
|
||||
// Wire start page signals — each closes the dialog then performs action
|
||||
connect(m_startPage, &StartPageWidget::openProject, this, [this]() {
|
||||
dismissStartPage();
|
||||
openFile();
|
||||
if (m_tabs.isEmpty()) showStartPage();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::newClass, this, [this]() {
|
||||
dismissStartPage();
|
||||
newClass();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::importSource, this, [this]() {
|
||||
dismissStartPage();
|
||||
importFromSource();
|
||||
if (m_tabs.isEmpty()) showStartPage();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::importXml, this, [this]() {
|
||||
dismissStartPage();
|
||||
importReclassXml();
|
||||
if (m_tabs.isEmpty()) showStartPage();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::importPdb, this, [this]() {
|
||||
dismissStartPage();
|
||||
importPdb();
|
||||
if (m_tabs.isEmpty()) showStartPage();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::continueClicked, this, [this]() {
|
||||
dismissStartPage();
|
||||
selfTest();
|
||||
});
|
||||
connect(m_startPage, &StartPageWidget::fileSelected, this, [this](const QString& path) {
|
||||
dismissStartPage();
|
||||
project_open(path);
|
||||
});
|
||||
connect(m_startPage, &QDialog::rejected, this, [this]() {
|
||||
dismissStartPage();
|
||||
});
|
||||
|
||||
// Center over main window and show as application-modal
|
||||
m_startPage->move(geometry().center() - m_startPage->rect().center());
|
||||
m_startPage->open();
|
||||
}
|
||||
|
||||
void MainWindow::dismissStartPage() {
|
||||
if (!m_startPage) return;
|
||||
auto* sp = m_startPage;
|
||||
m_startPage = nullptr; // null first — close() may re-enter via rejected signal
|
||||
sp->close();
|
||||
sp->deleteLater();
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
// ── Entry point ──
|
||||
@@ -4043,11 +4195,9 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
window.show();
|
||||
|
||||
// Auto-open demo project from saved .rcx file
|
||||
QMetaObject::invokeMethod(&window, "selfTest");
|
||||
// Show VS2022-style start page instead of jumping straight to demo
|
||||
QMetaObject::invokeMethod(&window, "showStartPage", Qt::QueuedConnection);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
// DockTabButtons has Q_OBJECT in main.cpp — need the moc include
|
||||
#include "main.moc"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "titlebar.h"
|
||||
#include "pluginmanager.h"
|
||||
#include "scannerpanel.h"
|
||||
#include "startpage.h"
|
||||
#include <QMainWindow>
|
||||
#include <QLabel>
|
||||
#include <QSplitter>
|
||||
@@ -23,6 +24,7 @@ namespace rcx {
|
||||
class McpBridge;
|
||||
class ShimmerLabel;
|
||||
class DockGripWidget;
|
||||
class WorkspaceDelegate;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
@@ -90,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;
|
||||
|
||||
@@ -154,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
|
||||
@@ -169,9 +175,15 @@ private:
|
||||
DockGripWidget* m_scanDockGrip = nullptr;
|
||||
void createScannerDock();
|
||||
|
||||
// Start page
|
||||
StartPageWidget* m_startPage = nullptr;
|
||||
Q_INVOKABLE void showStartPage();
|
||||
void dismissStartPage();
|
||||
|
||||
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>
|
||||
@@ -64,5 +65,6 @@
|
||||
<file alias="pinned.svg">vsicons/pinned.svg</file>
|
||||
<file alias="close-all.svg">vsicons/close-all.svg</file>
|
||||
<file alias="split-vertical.svg">vsicons/split-vertical.svg</file>
|
||||
<file alias="book.svg">vsicons/book.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
360
src/startpage.h
Normal file
360
src/startpage.h
Normal file
@@ -0,0 +1,360 @@
|
||||
#pragma once
|
||||
#include "themes/thememanager.h"
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Single-widget start page: everything painted in paintEvent.
|
||||
// Zero CSS, zero Fusion conflicts, zero child-widget styling issues.
|
||||
|
||||
class StartPageWidget : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit StartPageWidget(QWidget* parent = nullptr) : QDialog(parent) {
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
m_search = new QLineEdit(this);
|
||||
m_search->setPlaceholderText("Search recent...");
|
||||
m_search->setFixedHeight(30);
|
||||
m_search->setMaximumWidth(330);
|
||||
m_search->addAction(QIcon(":/vsicons/search.svg"), QLineEdit::TrailingPosition);
|
||||
connect(m_search, &QLineEdit::textChanged, this, [this]{ buildGroups(); update(); });
|
||||
|
||||
loadEntries();
|
||||
buildGroups();
|
||||
applyTheme(ThemeManager::instance().current());
|
||||
}
|
||||
|
||||
void applyTheme(const Theme& t) {
|
||||
m_t = t;
|
||||
m_search->setStyleSheet(
|
||||
"QLineEdit { background: " + t.background.name() + "; color: " + t.text.name()
|
||||
+ "; border: 1px solid " + t.border.name()
|
||||
+ "; padding: 2px 8px; font-size: 13px; }"
|
||||
"QLineEdit:focus { border: 1px solid " + t.borderFocused.name() + "; }");
|
||||
update();
|
||||
}
|
||||
|
||||
signals:
|
||||
void openProject();
|
||||
void newClass();
|
||||
void importSource();
|
||||
void importXml();
|
||||
void importPdb();
|
||||
void continueClicked();
|
||||
void fileSelected(const QString& path);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
const int LX = 48, TM = 36, RM = 32, GAP = 40, RW = 340;
|
||||
const int rpX = width() - RW - RM;
|
||||
const int lW = qMax(100, rpX - GAP - LX);
|
||||
|
||||
p.fillRect(rect(), m_t.background);
|
||||
|
||||
// ── Title ──
|
||||
int y = TM;
|
||||
QFont titleF = font(); titleF.setPixelSize(30); titleF.setWeight(QFont::Light);
|
||||
p.setFont(titleF); p.setPen(m_t.text);
|
||||
QFontMetrics titleFm(titleF);
|
||||
p.drawText(LX, y + titleFm.ascent(), "Reclass");
|
||||
y += titleFm.height() + 24;
|
||||
|
||||
// ── Headings (left + right at same y) ──
|
||||
QFont headF = font(); headF.setPixelSize(20); headF.setWeight(QFont::DemiBold);
|
||||
p.setFont(headF); QFontMetrics headFm(headF);
|
||||
p.drawText(LX, y + headFm.ascent(), "Open recent");
|
||||
int ry = y;
|
||||
p.drawText(rpX, ry + headFm.ascent(), "Get started");
|
||||
ry += headFm.height() + 14;
|
||||
y += headFm.height() + 14;
|
||||
|
||||
// ── Search bar (only child widget) ──
|
||||
m_search->setGeometry(LX, y, qMin(330, lW), 30);
|
||||
y += 46;
|
||||
m_listTop = y;
|
||||
|
||||
// ── Right panel ──
|
||||
drawCards(p, rpX, ry, RW);
|
||||
|
||||
// ── File list ──
|
||||
drawFileList(p, LX, lW);
|
||||
|
||||
// ── Border ──
|
||||
p.setPen(QPen(m_t.border, 1));
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRect(rect().adjusted(0, 0, -1, -1));
|
||||
}
|
||||
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
auto [z, i] = hitTest(e->pos());
|
||||
if (z != m_hz || i != m_hi) {
|
||||
m_hz = z; m_hi = i;
|
||||
setCursor(z != HZ_None ? Qt::PointingHandCursor : Qt::ArrowCursor);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void mousePressEvent(QMouseEvent* e) override {
|
||||
if (e->button() != Qt::LeftButton) return;
|
||||
auto [z, i] = hitTest(e->pos());
|
||||
if (z == HZ_Entry) emit fileSelected(m_filtered[i].path);
|
||||
if (z == HZ_Group) { m_groups[i].expanded = !m_groups[i].expanded; update(); }
|
||||
if (z == HZ_Card && i == 0) emit newClass();
|
||||
if (z == HZ_Card && i == 1) emit openProject();
|
||||
if (z == HZ_Card && i == 2) emit importSource();
|
||||
if (z == HZ_Card && i == 3) emit importXml();
|
||||
if (z == HZ_Card && i == 4) emit importPdb();
|
||||
if (z == HZ_Continue) emit continueClicked();
|
||||
}
|
||||
|
||||
void wheelEvent(QWheelEvent* e) override {
|
||||
m_scrollY = qBound(0, m_scrollY - e->angleDelta().y() / 2, m_maxScroll);
|
||||
update();
|
||||
}
|
||||
|
||||
void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); update(); }
|
||||
void leaveEvent(QEvent*) override { m_hz = HZ_None; m_hi = -1; setCursor(Qt::ArrowCursor); update(); }
|
||||
void keyPressEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Escape) reject(); }
|
||||
|
||||
private:
|
||||
enum HZ { HZ_None, HZ_Entry, HZ_Group, HZ_Card, HZ_Continue };
|
||||
struct Hit { HZ zone; int idx; };
|
||||
|
||||
struct Entry {
|
||||
QString path, fileName, dirPath;
|
||||
QDateTime lastModified;
|
||||
bool isExample;
|
||||
};
|
||||
struct Group {
|
||||
QString name;
|
||||
bool expanded = true;
|
||||
QVector<int> entries;
|
||||
};
|
||||
|
||||
Theme m_t;
|
||||
QLineEdit* m_search;
|
||||
QVector<Entry> m_all, m_filtered;
|
||||
QVector<Group> m_groups;
|
||||
int m_scrollY = 0, m_maxScroll = 0, m_listTop = 0, m_contentH = 0;
|
||||
|
||||
HZ m_hz = HZ_None;
|
||||
int m_hi = -1;
|
||||
|
||||
// Hit rects populated during paint
|
||||
QVector<QPair<int, QRectF>> m_grpRects, m_entRects;
|
||||
QRectF m_cardR[5], m_contR;
|
||||
|
||||
void drawIcon(QPainter& p, const QString& path, int x, int y, int sz) {
|
||||
QIcon(path).paint(&p, x, y, sz, sz);
|
||||
}
|
||||
|
||||
// ── Data loading ──
|
||||
|
||||
void loadEntries() {
|
||||
m_all.clear();
|
||||
QSettings s("Reclass", "Reclass");
|
||||
for (const auto& path : s.value("recentFiles").toStringList()) {
|
||||
QFileInfo fi(path);
|
||||
if (!fi.exists()) continue;
|
||||
m_all.append({fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
|
||||
fi.lastModified(), false});
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||
#else
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
#endif
|
||||
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
|
||||
m_all.append({exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
|
||||
QFileInfo(exDir.filePath(fn)).lastModified(), true});
|
||||
}
|
||||
|
||||
void buildGroups() {
|
||||
QString f = m_search->text().trimmed().toLower();
|
||||
m_filtered.clear();
|
||||
for (const auto& e : m_all)
|
||||
if (f.isEmpty() || e.fileName.toLower().contains(f) || e.dirPath.toLower().contains(f))
|
||||
m_filtered.append(e);
|
||||
|
||||
QDate today = QDate::currentDate();
|
||||
QVector<int> bk[6];
|
||||
for (int i = 0; i < m_filtered.size(); i++) {
|
||||
auto& e = m_filtered[i];
|
||||
if (e.isExample) { bk[5].append(i); continue; }
|
||||
int d = e.lastModified.date().daysTo(today);
|
||||
if (d == 0) bk[0].append(i);
|
||||
else if (d == 1) bk[1].append(i);
|
||||
else if (d < 7) bk[2].append(i);
|
||||
else if (e.lastModified.date().month() == today.month()
|
||||
&& e.lastModified.date().year() == today.year()) bk[3].append(i);
|
||||
else bk[4].append(i);
|
||||
}
|
||||
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
|
||||
m_groups.clear();
|
||||
for (int i = 0; i < 6; i++)
|
||||
if (!bk[i].isEmpty()) m_groups.append({names[i], true, bk[i]});
|
||||
m_scrollY = 0;
|
||||
}
|
||||
|
||||
// ── Drawing ──
|
||||
|
||||
void drawCards(QPainter& p, int x, int y, int w) {
|
||||
struct C { const char* icon; const char* title; const char* desc; };
|
||||
static const C cards[] = {
|
||||
{":/vsicons/symbol-structure.svg", "New Class", "Start a new binary class definition"},
|
||||
{":/vsicons/folder-opened.svg", "Open project", "Open an existing .rcx project"},
|
||||
{":/vsicons/file-binary.svg", "Import from Source", "Import C/C++ header or source file"},
|
||||
{":/vsicons/code.svg", "Import ReClass XML", "Import from ReClass .xml format"},
|
||||
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
|
||||
};
|
||||
|
||||
const int N = 5, CH = 84, R = 6, panelH = N * CH;
|
||||
|
||||
// Rounded panel background
|
||||
QPainterPath clip;
|
||||
clip.addRoundedRect(QRectF(x, y, w, panelH), R, R);
|
||||
p.save();
|
||||
p.setClipPath(clip);
|
||||
p.fillRect(x, y, w, panelH, m_t.background);
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
int cy = y + i * CH;
|
||||
QRectF cr(x, cy, w, CH);
|
||||
m_cardR[i] = cr;
|
||||
bool hov = (m_hz == HZ_Card && m_hi == i);
|
||||
|
||||
if (hov) {
|
||||
p.fillRect(cr, m_t.hover);
|
||||
p.fillRect(QRectF(x, cy, 3, CH), m_t.indHoverSpan);
|
||||
}
|
||||
|
||||
// Icon (32px, centered vertically)
|
||||
int iconSz = 32;
|
||||
drawIcon(p, cards[i].icon, x + 24, cy + (CH - iconSz) / 2, iconSz);
|
||||
|
||||
// Title + description block, centered vertically
|
||||
int tx = x + 24 + iconSz + 16;
|
||||
QFont tf = font(); tf.setPixelSize(15);
|
||||
QFont df = font(); df.setPixelSize(12);
|
||||
QFontMetrics tfm(tf), dfm(df);
|
||||
int blockH = tfm.height() + 5 + dfm.height();
|
||||
int by = cy + (CH - blockH) / 2;
|
||||
|
||||
p.setFont(tf); p.setPen(m_t.text);
|
||||
p.drawText(tx, by + tfm.ascent(), cards[i].title);
|
||||
p.setFont(df); p.setPen(m_t.textDim);
|
||||
p.drawText(tx, by + tfm.height() + 5 + dfm.ascent(), cards[i].desc);
|
||||
}
|
||||
|
||||
p.restore();
|
||||
|
||||
// "Continue →" centered under the panel
|
||||
int cy = y + panelH + 8;
|
||||
QFont lf = font(); lf.setPixelSize(13);
|
||||
if (m_hz == HZ_Continue) lf.setUnderline(true);
|
||||
p.setFont(lf); p.setPen(m_t.indHoverSpan);
|
||||
QFontMetrics lfm(lf);
|
||||
QString ct = QStringLiteral("Continue \u2192");
|
||||
int cw = lfm.horizontalAdvance(ct);
|
||||
m_contR = QRectF(x + (w - cw) / 2, cy, cw, lfm.height());
|
||||
p.drawText(int(m_contR.x()), cy + lfm.ascent(), ct);
|
||||
}
|
||||
|
||||
void drawFileList(QPainter& p, int x, int w) {
|
||||
int listH = height() - 24 - m_listTop;
|
||||
p.save();
|
||||
p.setClipRect(x, m_listTop, w, listH);
|
||||
|
||||
int fy = m_listTop - m_scrollY;
|
||||
m_grpRects.clear();
|
||||
m_entRects.clear();
|
||||
|
||||
for (int gi = 0; gi < m_groups.size(); gi++) {
|
||||
auto& g = m_groups[gi];
|
||||
if (gi > 0) fy += 15;
|
||||
|
||||
// Group header
|
||||
m_grpRects.append({gi, QRectF(x, fy, w, 28)});
|
||||
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
|
||||
int triX = x + 8, triY = fy + 11;
|
||||
QPolygonF tri;
|
||||
if (g.expanded) tri << QPointF(triX,triY) << QPointF(triX+6,triY) << QPointF(triX+3,triY+6);
|
||||
else tri << QPointF(triX,triY) << QPointF(triX+6,triY+3) << QPointF(triX,triY+6);
|
||||
p.drawPolygon(tri);
|
||||
|
||||
QFont gf = font(); gf.setPixelSize(13);
|
||||
p.setFont(gf); p.setPen(m_t.text);
|
||||
p.drawText(triX + 14, fy + 14 + QFontMetrics(gf).ascent() / 2 - 1, g.name);
|
||||
fy += 28;
|
||||
|
||||
if (!g.expanded) continue;
|
||||
|
||||
for (int ei : g.entries) {
|
||||
auto& e = m_filtered[ei];
|
||||
QRectF er(x, fy, w, 52);
|
||||
m_entRects.append({ei, er});
|
||||
if (m_hz == HZ_Entry && m_hi == ei) p.fillRect(er, m_t.hover);
|
||||
|
||||
drawIcon(p, e.isExample ? ":/vsicons/book.svg" : ":/vsicons/symbol-structure.svg",
|
||||
x + 24, fy + 17, 18);
|
||||
|
||||
int tx = x + 52, avail = w - 64;
|
||||
QFont nf = font(); nf.setPixelSize(14);
|
||||
p.setFont(nf); p.setPen(m_t.text);
|
||||
QFontMetrics nm(nf);
|
||||
int ny = fy + 8;
|
||||
p.drawText(tx, ny + nm.ascent(),
|
||||
nm.elidedText(e.fileName, Qt::ElideMiddle, avail * 0.65));
|
||||
|
||||
if (!e.isExample) {
|
||||
p.setPen(m_t.textDim);
|
||||
QString dt = e.lastModified.toString("M/d/yyyy h:mm AP");
|
||||
p.drawText(x + w - 12 - nm.horizontalAdvance(dt), ny + nm.ascent(), dt);
|
||||
}
|
||||
|
||||
QFont pf = font(); pf.setPixelSize(12);
|
||||
p.setFont(pf); p.setPen(m_t.textDim);
|
||||
QFontMetrics pm(pf);
|
||||
p.drawText(tx, ny + nm.height() + 4 + pm.ascent(),
|
||||
pm.elidedText(e.dirPath, Qt::ElideMiddle, avail));
|
||||
fy += 52;
|
||||
}
|
||||
}
|
||||
|
||||
m_contentH = fy + m_scrollY - m_listTop;
|
||||
m_maxScroll = qMax(0, m_contentH - listH);
|
||||
p.restore();
|
||||
}
|
||||
|
||||
// ── Hit testing ──
|
||||
|
||||
Hit hitTest(QPoint pos) const {
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (m_cardR[i].contains(pos)) return {HZ_Card, i};
|
||||
if (m_contR.contains(pos)) return {HZ_Continue, 0};
|
||||
if (pos.y() >= m_listTop && pos.y() < height() - 24) {
|
||||
for (const auto& [gi, r] : m_grpRects)
|
||||
if (r.contains(pos)) return {HZ_Group, gi};
|
||||
for (const auto& [ei, r] : m_entRects)
|
||||
if (r.contains(pos)) return {HZ_Entry, ei};
|
||||
}
|
||||
return {HZ_None, -1};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
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"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "titlebar.h"
|
||||
#include "themes/thememanager.h"
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QStyle>
|
||||
@@ -76,15 +77,35 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.text.name()));
|
||||
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
// Menu bar palette — all roles used by MenuBarStyle, so live theme
|
||||
// switches don't rely on app-palette inheritance (which can stall
|
||||
// once setPalette has been called on a widget).
|
||||
{
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.text);
|
||||
mbPal.setColor(QPalette::Text, theme.text);
|
||||
mbPal.setColor(QPalette::Highlight, theme.selected);
|
||||
mbPal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
mbPal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
mbPal.setColor(QPalette::Dark, theme.border);
|
||||
mbPal.setColor(QPalette::Mid, theme.hover);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
|
||||
// Propagate to existing QMenu children so dropdown popups update too
|
||||
for (auto* menu : m_menuBar->findChildren<QMenu*>()) {
|
||||
QPalette mp = menu->palette();
|
||||
mp.setColor(QPalette::Window, theme.background);
|
||||
mp.setColor(QPalette::WindowText, theme.text);
|
||||
mp.setColor(QPalette::Text, theme.text);
|
||||
mp.setColor(QPalette::Highlight, theme.selected);
|
||||
mp.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
mp.setColor(QPalette::AlternateBase, theme.surface);
|
||||
mp.setColor(QPalette::Dark, theme.border);
|
||||
menu->setPalette(mp);
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
|
||||
@@ -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,92 +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));
|
||||
};
|
||||
|
||||
// Sort structs by visible children count descending (most fields first)
|
||||
auto countVisible = [&](const Entry& e) {
|
||||
int n = 0;
|
||||
for (int idx : e.tree->childrenOf(e.node->id))
|
||||
if (!isHexPad(e.tree->nodes[idx].kind)) ++n;
|
||||
return n;
|
||||
};
|
||||
auto cmpChildren = [&](const Entry& a, const Entry& b) {
|
||||
int ca = countVisible(a);
|
||||
int cb = countVisible(b);
|
||||
if (ca != cb) return ca > cb;
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpChildren);
|
||||
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
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