mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
14 Commits
v2027.02.1
...
latest-lin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71bc51cbab | ||
|
|
60a97ab81b | ||
|
|
bb00e75019 | ||
|
|
c038c59e34 | ||
|
|
862f76b984 | ||
|
|
818285a76e | ||
|
|
ef5e2ebdb9 | ||
|
|
75fedd2222 | ||
|
|
389745e501 | ||
|
|
1473a58742 | ||
|
|
4192a4dad3 | ||
|
|
4c6bb9564f | ||
|
|
0ef9841f90 | ||
|
|
0a8244dad4 |
153
.github/workflows/build.yml
vendored
Normal file
153
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_msvc2022_64'
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_windbg_provider|test_com_security"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-Windows-x64
|
||||
path: |
|
||||
build/Reclass.exe
|
||||
build/ReclassMcpBridge.exe
|
||||
build/Plugins/
|
||||
build/*.dll
|
||||
build/platforms/
|
||||
build/styles/
|
||||
build/imageformats/
|
||||
build/iconengines/
|
||||
build/themes/
|
||||
build/screenshot.png
|
||||
|
||||
- name: Package release zip
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp build/Reclass.exe release/
|
||||
cp build/ReclassMcpBridge.exe release/
|
||||
cp build/*.dll release/ 2>/dev/null || true
|
||||
cp -r build/platforms release/ 2>/dev/null || true
|
||||
cp -r build/styles release/ 2>/dev/null || true
|
||||
cp -r build/imageformats release/ 2>/dev/null || true
|
||||
cp -r build/iconengines release/ 2>/dev/null || true
|
||||
cp -r build/Plugins release/ 2>/dev/null || true
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
cp build/screenshot.png release/ 2>/dev/null || true
|
||||
cd release && 7z a ../win64-reclass-latest.zip *
|
||||
|
||||
- name: Update win64 release
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: latest-win64
|
||||
name: win64
|
||||
body: |
|
||||
Windows x64 build from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: true
|
||||
files: win64-reclass-latest.zip
|
||||
make_latest: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build libgl1-mesa-dev
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor"
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-Linux-x64
|
||||
path: |
|
||||
build/Reclass
|
||||
build/ReclassMcpBridge
|
||||
|
||||
- name: Package release tarball
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp build/Reclass release/
|
||||
cp build/ReclassMcpBridge release/
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
tar czf linux64-reclass-latest.tar.gz -C release .
|
||||
|
||||
- name: Update linux64 release
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: latest-linux64
|
||||
name: linux64
|
||||
body: |
|
||||
Linux x64 build from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: true
|
||||
files: linux64-reclass-latest.tar.gz
|
||||
make_latest: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -60,6 +60,8 @@ add_executable(Reclass
|
||||
src/themes/themeeditor.h
|
||||
src/themes/themeeditor.cpp
|
||||
src/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/mcp/mcp_bridge.h
|
||||
@@ -95,12 +97,14 @@ endforeach()
|
||||
|
||||
include(deploy)
|
||||
|
||||
add_custom_target(screenshot ALL
|
||||
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||
DEPENDS Reclass deploy
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Capturing UI screenshot with class open..."
|
||||
)
|
||||
if(TARGET deploy)
|
||||
add_custom_target(screenshot ALL
|
||||
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||
DEPENDS Reclass deploy
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Capturing UI screenshot with class open..."
|
||||
)
|
||||
endif()
|
||||
|
||||
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
||||
file(WRITE ${_combine_script} "
|
||||
@@ -257,15 +261,14 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_theme COMMAND test_theme)
|
||||
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_windbg_provider PRIVATE dbgeng ole32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
|
||||
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
|
||||
# Requires a running WinDbg debug server on port 5055
|
||||
@@ -288,5 +291,7 @@ if(BUILD_TESTING)
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemoryWindows)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
endif()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ProcessMemoryWindowsPlugin LANGUAGES CXX)
|
||||
project(ProcessMemoryPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Plugin sources
|
||||
set(PLUGIN_SOURCES
|
||||
ProcessMemoryWindowsPlugin.h
|
||||
ProcessMemoryWindowsPlugin.cpp
|
||||
ProcessMemoryPlugin.h
|
||||
ProcessMemoryPlugin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES})
|
||||
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Platform-specific linking
|
||||
if(WIN32)
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32)
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
|
||||
endif()
|
||||
|
||||
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden)
|
||||
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE
|
||||
target_include_directories(ProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES
|
||||
set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "ProcessMemoryWindowsPlugin.h"
|
||||
#include "ProcessMemoryPlugin.h"
|
||||
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsProvider implementation
|
||||
// ProcessMemoryProvider implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_handle(nullptr)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -60,7 +60,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || len <= 0) return false;
|
||||
|
||||
@@ -71,7 +71,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
return bytesRead > 0;
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_writable || len <= 0) return false;
|
||||
|
||||
@@ -81,7 +81,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
@@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_fd(-1)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -152,7 +152,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (m_fd < 0 || len <= 0) return false;
|
||||
|
||||
@@ -176,7 +176,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
return nread == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (m_fd < 0 || !m_writable || len <= 0) return false;
|
||||
|
||||
@@ -200,7 +200,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
|
||||
return nwritten == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -215,7 +215,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
// Parse /proc/<pid>/maps to discover loaded modules
|
||||
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
|
||||
@@ -288,7 +288,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#endif // platform
|
||||
|
||||
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_handle)
|
||||
@@ -299,7 +299,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
#endif
|
||||
}
|
||||
|
||||
int ProcessMemoryWindowsProvider::size() const
|
||||
int ProcessMemoryProvider::size() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return m_handle ? 0x10000 : 0;
|
||||
@@ -309,22 +309,22 @@ int ProcessMemoryWindowsProvider::size() const
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsPlugin implementation
|
||||
// ProcessMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
QIcon ProcessMemoryWindowsPlugin::Icon() const
|
||||
QIcon ProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const
|
||||
bool ProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
// Target format: "pid:name" or just "pid"
|
||||
QRegularExpression re("^\\d+");
|
||||
return re.match(target).hasMatch();
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
// Parse target: "pid:name" or just "pid"
|
||||
QStringList parts = target.split(':');
|
||||
@@ -339,7 +339,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
|
||||
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
|
||||
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
|
||||
if (!provider->isValid())
|
||||
{
|
||||
if (errorMsg)
|
||||
@@ -352,7 +352,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const
|
||||
uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Parse PID from target
|
||||
@@ -409,7 +409,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Use custom process enumeration from plugin
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
@@ -440,7 +440,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> processes;
|
||||
|
||||
@@ -543,5 +543,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new ProcessMemoryWindowsPlugin();
|
||||
return new ProcessMemoryPlugin();
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Process memory provider (Windows)
|
||||
* Reads/writes memory from a live process using Windows platform APIs
|
||||
* Process memory provider
|
||||
* Reads/writes memory from a live process using platform APIs
|
||||
*/
|
||||
class ProcessMemoryWindowsProvider : public rcx::Provider
|
||||
class ProcessMemoryProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryWindowsProvider() override;
|
||||
ProcessMemoryProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
@@ -57,15 +57,15 @@ private:
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin that provides ProcessMemoryWindowsProvider
|
||||
* Plugin that provides ProcessMemoryProvider
|
||||
*/
|
||||
class ProcessMemoryWindowsPlugin : public IProviderPlugin
|
||||
class ProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
std::string Name() const override { return "Process Memory Windows"; }
|
||||
std::string Name() const override { return "Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override { return "Read and write memory from local running processes (Windows)"; }
|
||||
std::string Description() const override { return "Read and write memory from local running processes"; }
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 63 KiB |
@@ -14,8 +14,9 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
|
||||
struct ComposeState {
|
||||
QString text;
|
||||
QVector<LineMeta> meta;
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
|
||||
int currentLine = 0;
|
||||
int typeW = kColType; // global type column width (fallback)
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
@@ -64,7 +65,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
|
||||
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
||||
uint32_t mask = 0;
|
||||
if (isCont) mask |= (1u << M_CONT);
|
||||
if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD);
|
||||
// No ambient validation markers — errors only shown during inline editing.
|
||||
return mask;
|
||||
}
|
||||
@@ -118,14 +118,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
int typeW = state.effectiveTypeW(scopeId);
|
||||
int nameW = state.effectiveNameW(scopeId);
|
||||
|
||||
// Line count: padding wraps at 8 bytes per line
|
||||
int numLines;
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalBytes = qMax(1, node.arrayLen);
|
||||
numLines = (totalBytes + 7) / 8;
|
||||
} else {
|
||||
numLines = linesForKind(node.kind);
|
||||
}
|
||||
int numLines = linesForKind(node.kind);
|
||||
|
||||
// Resolve pointer target name for display
|
||||
QString ptrTypeOverride;
|
||||
@@ -156,12 +149,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalSz = qMax(1, node.arrayLen);
|
||||
lm.lineByteCount = qMin(8, totalSz - sub * 8);
|
||||
} else {
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
@@ -430,29 +418,42 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
QVector<int> ptrChildren = state.childMap.value(node.id);
|
||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||
|
||||
// Force collapsed if this refId is already being virtually expanded
|
||||
// (prevents infinite recursion in virtual expansion mode).
|
||||
// Materialized children bypass this — they are real tree nodes with
|
||||
// independent collapsed state, so recursion is bounded by the tree.
|
||||
bool forceCollapsed = !hasMaterialized
|
||||
&& state.virtualPtrRefs.contains(node.refId);
|
||||
bool effectiveCollapsed = node.collapsed || forceCollapsed;
|
||||
|
||||
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldCollapsed = effectiveCollapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW), lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed) {
|
||||
if (!effectiveCollapsed) {
|
||||
int sz = node.byteSize();
|
||||
uint64_t ptrVal = 0;
|
||||
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
||||
@@ -480,18 +481,42 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
if (!ptrReadable)
|
||||
pBase = (uint64_t)0 - tree.baseAddress;
|
||||
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
if (hasMaterialized) {
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
// so resolveAddr computes offsets relative to the pointer target.
|
||||
std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
for (int childIdx : ptrChildren) {
|
||||
composeNode(state, tree, childProv, childIdx, depth + 1,
|
||||
pBase, node.id, false, node.id);
|
||||
}
|
||||
} else {
|
||||
// Virtual expansion via ref struct definition.
|
||||
// Temporarily remove the ref struct from visiting so composeParent
|
||||
// doesn't hit the struct-level cycle guard. The ptrVisiting mechanism
|
||||
// handles actual address-level pointer cycles, and virtualPtrRefs
|
||||
// prevents infinite virtual recursion (inner self-referential pointers
|
||||
// are force-collapsed with M_CYCLE for the user to materialize).
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
|
||||
bool wasVisiting = state.visiting.remove(node.refId);
|
||||
state.virtualPtrRefs.insert(node.refId);
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
state.virtualPtrRefs.remove(node.refId);
|
||||
if (wasVisiting) state.visiting.insert(node.refId);
|
||||
}
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
|
||||
// Footer for pointer fold
|
||||
@@ -571,7 +596,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip hex/padding (they show ASCII preview, not name column)
|
||||
// Skip hex (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
@@ -590,7 +615,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, but include containers)
|
||||
// Name width (skip hex, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||
}
|
||||
@@ -622,7 +647,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, include containers)
|
||||
// Name width (skip hex, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
|
||||
@@ -178,6 +178,14 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
||||
editor->applyDocument(m_lastResult);
|
||||
}
|
||||
updateCommandRow();
|
||||
|
||||
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
|
||||
if (!m_cachedPopup) {
|
||||
QTimer::singleShot(0, this, [this, editor]() {
|
||||
if (!m_cachedPopup && !m_editors.isEmpty())
|
||||
ensurePopup(editor);
|
||||
});
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
|
||||
@@ -226,8 +234,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
switch (target) {
|
||||
case EditTarget::Name: {
|
||||
if (text.isEmpty()) break;
|
||||
if (nodeIdx >= m_doc->tree.nodes.size()) break;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
// ASCII edit on Hex/Padding nodes
|
||||
// ASCII edit on Hex nodes
|
||||
if (isHexPreview(node.kind)) {
|
||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
||||
} else {
|
||||
@@ -602,7 +611,7 @@ void RcxController::refresh() {
|
||||
|
||||
if (isHexPreview(node.kind)) {
|
||||
// Per-byte tracking for hex preview nodes
|
||||
int lineOff = (node.kind == NodeKind::Padding) ? lm.subLine * 8 : 0;
|
||||
int lineOff = 0;
|
||||
int byteCount = lm.lineByteCount;
|
||||
for (int b = 0; b < byteCount; b++) {
|
||||
if (m_changedOffsets.contains(offset + lineOff + b)) {
|
||||
@@ -799,38 +808,52 @@ void RcxController::materializeRefChildren(int nodeIdx) {
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||
auto& tree = m_doc->tree;
|
||||
|
||||
// Snapshot values before addNode invalidates references
|
||||
const uint64_t parentId = tree.nodes[nodeIdx].id;
|
||||
const uint64_t refId = tree.nodes[nodeIdx].refId;
|
||||
// Snapshot values before any mutation invalidates references
|
||||
const uint64_t parentId = tree.nodes[nodeIdx].id;
|
||||
const uint64_t refId = tree.nodes[nodeIdx].refId;
|
||||
const NodeKind parentKind = tree.nodes[nodeIdx].kind;
|
||||
const QString parentName = tree.nodes[nodeIdx].name;
|
||||
|
||||
if (refId == 0) return;
|
||||
if (!tree.childrenOf(parentId).isEmpty()) return; // already materialized
|
||||
|
||||
// Clone all children of the referenced struct as real children of this struct
|
||||
// Collect children to clone (copy by value to avoid reference invalidation)
|
||||
QVector<int> refChildren = tree.childrenOf(refId);
|
||||
if (refChildren.isEmpty()) return;
|
||||
|
||||
QVector<Node> clones;
|
||||
clones.reserve(refChildren.size());
|
||||
for (int ci : refChildren) {
|
||||
Node copy = tree.nodes[ci];
|
||||
copy.id = 0; // auto-assign new ID
|
||||
Node copy = tree.nodes[ci]; // copy by value before any mutation
|
||||
copy.id = tree.reserveId();
|
||||
copy.parentId = parentId;
|
||||
copy.collapsed = true; // start collapsed
|
||||
tree.addNode(copy);
|
||||
copy.collapsed = true;
|
||||
clones.append(copy);
|
||||
}
|
||||
|
||||
// Wrap all mutations in an undo macro
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Materialize ref children"));
|
||||
|
||||
for (const Node& clone : clones) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Insert{clone, {}}));
|
||||
}
|
||||
tree.invalidateIdCache();
|
||||
|
||||
// Auto-expand the self-referential child (the one that was the cycle)
|
||||
// so the user gets expand in a single click
|
||||
QVector<int> newChildren = tree.childrenOf(parentId);
|
||||
for (int ci : newChildren) {
|
||||
auto& c = tree.nodes[ci];
|
||||
if (c.kind == parentKind && c.name == parentName && c.refId == refId) {
|
||||
c.collapsed = false;
|
||||
for (const Node& clone : clones) {
|
||||
if (clone.kind == parentKind && clone.name == parentName && clone.refId == refId) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Collapse{clone.id, true, false}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
if (!m_suppressRefresh) refresh();
|
||||
}
|
||||
|
||||
void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
@@ -1093,23 +1116,23 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
// Quick-convert suggestions for Hex nodes
|
||||
bool addedQuickConvert = false;
|
||||
if (node.kind == NodeKind::Hex64) {
|
||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint64_t", [this, nodeId]() {
|
||||
menu.addAction("Change to uint64_t", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt64);
|
||||
});
|
||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() {
|
||||
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||
});
|
||||
addedQuickConvert = true;
|
||||
} else if (node.kind == NodeKind::Hex32) {
|
||||
menu.addAction(icon("symbol-numeric.svg"), "Change to uint32_t", [this, nodeId]() {
|
||||
menu.addAction("Change to uint32_t", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||
});
|
||||
addedQuickConvert = true;
|
||||
} else if (node.kind == NodeKind::Hex16) {
|
||||
menu.addAction(icon("symbol-numeric.svg"), "Change to int16_t", [this, nodeId]() {
|
||||
menu.addAction("Change to int16_t", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::Int16);
|
||||
});
|
||||
@@ -1119,7 +1142,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
menu.addSeparator();
|
||||
|
||||
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
|
||||
&& node.kind != NodeKind::Padding
|
||||
&& m_doc->provider->isWritable();
|
||||
if (isEditable) {
|
||||
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
|
||||
@@ -1135,6 +1157,51 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
editor->beginInlineEdit(EditTarget::Type, line);
|
||||
});
|
||||
|
||||
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
|
||||
if (!isHexNode(node.kind) && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) {
|
||||
menu.addAction("Convert to &Hex", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
const Node& n = m_doc->tree.nodes[ni];
|
||||
int totalSize = n.byteSize();
|
||||
if (totalSize <= 0) return;
|
||||
|
||||
uint64_t parentId = n.parentId;
|
||||
int baseOffset = n.offset;
|
||||
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Convert to Hex"));
|
||||
|
||||
// Remove the original node
|
||||
QVector<Node> subtree;
|
||||
subtree.append(n);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Remove{nodeId, subtree, {}}));
|
||||
|
||||
// Insert hex nodes to fill the space (largest first)
|
||||
int padOffset = baseOffset;
|
||||
int gap = totalSize;
|
||||
while (gap > 0) {
|
||||
NodeKind padKind;
|
||||
int padSize;
|
||||
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
||||
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
||||
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
||||
else { padKind = NodeKind::Hex8; padSize = 1; }
|
||||
|
||||
insertNode(parentId, padOffset, padKind,
|
||||
QString("pad_%1").arg(padOffset, 2, 16, QChar('0')));
|
||||
padOffset += padSize;
|
||||
gap -= padSize;
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
if (!m_suppressRefresh) refresh();
|
||||
});
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
||||
@@ -1396,17 +1463,17 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
// Separate into real nodes (non-Padding) and padding nodes
|
||||
// Separate into real nodes (non-hex) and hex filler nodes
|
||||
struct NodeInfo { uint64_t id; int offset; int size; };
|
||||
QVector<NodeInfo> realNodes;
|
||||
QVector<uint64_t> padIds;
|
||||
QVector<uint64_t> hexIds;
|
||||
|
||||
for (int ci : kids) {
|
||||
const Node& child = tree.nodes[ci];
|
||||
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
? tree.structSpan(child.id) : child.byteSize();
|
||||
if (child.kind == NodeKind::Padding)
|
||||
padIds.append(child.id);
|
||||
if (isHexNode(child.kind))
|
||||
hexIds.append(child.id);
|
||||
else
|
||||
realNodes.append({child.id, child.offset, sz});
|
||||
}
|
||||
@@ -1439,7 +1506,7 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
||||
}
|
||||
|
||||
// Check if anything actually changes
|
||||
if (offChanges.isEmpty() && padIds.isEmpty() && padsNeeded.isEmpty())
|
||||
if (offChanges.isEmpty() && hexIds.isEmpty() && padsNeeded.isEmpty())
|
||||
return;
|
||||
|
||||
// Apply as undoable macro
|
||||
@@ -1447,14 +1514,14 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
|
||||
|
||||
// 1. Remove all existing Padding nodes (no offset adjustments — we recompute)
|
||||
for (uint64_t pid : padIds) {
|
||||
int idx = tree.indexOfId(pid);
|
||||
// 1. Remove all existing hex filler nodes (no offset adjustments — we recompute)
|
||||
for (uint64_t hid : hexIds) {
|
||||
int idx = tree.indexOfId(hid);
|
||||
if (idx < 0) continue;
|
||||
QVector<Node> subtree;
|
||||
subtree.append(tree.nodes[idx]);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Remove{pid, subtree, {}}));
|
||||
cmd::Remove{hid, subtree, {}}));
|
||||
}
|
||||
|
||||
// 2. Reposition real nodes
|
||||
@@ -1463,15 +1530,28 @@ void RcxController::performRealignment(uint64_t structId, int targetAlign) {
|
||||
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
|
||||
}
|
||||
|
||||
// 3. Insert new padding in gaps
|
||||
// 3. Insert hex nodes to fill gaps (largest first for alignment)
|
||||
for (const auto& pi : padsNeeded) {
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Padding;
|
||||
pad.parentId = structId;
|
||||
pad.offset = pi.offset;
|
||||
pad.arrayLen = pi.size;
|
||||
pad.id = tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
|
||||
int padOffset = pi.offset;
|
||||
int gap = pi.size;
|
||||
while (gap > 0) {
|
||||
NodeKind padKind;
|
||||
int padSize;
|
||||
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
|
||||
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
|
||||
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
|
||||
else { padKind = NodeKind::Hex8; padSize = 1; }
|
||||
|
||||
Node pad;
|
||||
pad.kind = padKind;
|
||||
pad.parentId = structId;
|
||||
pad.offset = padOffset;
|
||||
pad.name = QString("pad_%1").arg(padOffset, 2, 16, QChar('0'));
|
||||
pad.id = tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
|
||||
padOffset += padSize;
|
||||
gap -= padSize;
|
||||
}
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
@@ -1575,7 +1655,6 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
|
||||
auto addPrimitives = [&](bool enabled, bool excludeStructArrayPad) {
|
||||
for (const auto& m : kKindMeta) {
|
||||
if (m.kind == NodeKind::Padding) continue;
|
||||
if (excludeStructArrayPad &&
|
||||
(m.kind == NodeKind::Struct || m.kind == NodeKind::Array))
|
||||
continue;
|
||||
@@ -1710,6 +1789,10 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
});
|
||||
connect(popup, &TypeSelectorPopup::createNewTypeRequested,
|
||||
this, [this, mode, nodeIdx]() {
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Create new type"));
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = QString();
|
||||
@@ -1717,6 +1800,16 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
|
||||
n.offset = 0;
|
||||
n.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
||||
|
||||
// Populate with default hex nodes (8 x Hex64 = 64 bytes)
|
||||
for (int i = 0; i < 8; i++) {
|
||||
insertNode(n.id, i * 8, NodeKind::Hex64,
|
||||
QString("field_%1").arg(i * 8, 2, 16, QChar('0')));
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
|
||||
TypeEntry newEntry;
|
||||
newEntry.entryKind = TypeEntry::Composite;
|
||||
newEntry.structId = n.id;
|
||||
@@ -1735,14 +1828,22 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
||||
}
|
||||
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
|
||||
// BUG-1 fix: Copy needed fields to locals before any mutation.
|
||||
// changeNodeKind() can trigger insertNode() → addNode() → nodes.append(),
|
||||
// which may reallocate the QVector, invalidating any reference into it.
|
||||
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
|
||||
const NodeKind nodeKind = m_doc->tree.nodes[nodeIdx].kind;
|
||||
const NodeKind elemKind = m_doc->tree.nodes[nodeIdx].elementKind;
|
||||
const uint64_t nodeRefId = m_doc->tree.nodes[nodeIdx].refId;
|
||||
const int arrLen = m_doc->tree.nodes[nodeIdx].arrayLen;
|
||||
|
||||
// Parse the full text for modifiers (e.g. "int32_t[10]", "Ball*")
|
||||
TypeSpec spec = parseTypeSpec(fullText);
|
||||
|
||||
if (mode == TypePopupMode::FieldType) {
|
||||
if (entry.entryKind == TypeEntry::Primitive) {
|
||||
if (entry.primitiveKind != node.kind)
|
||||
if (entry.primitiveKind != nodeKind)
|
||||
changeNodeKind(nodeIdx, entry.primitiveKind);
|
||||
} else if (entry.entryKind == TypeEntry::Composite) {
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
@@ -1751,34 +1852,34 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
||||
|
||||
if (spec.isPointer) {
|
||||
// Pointer modifier: e.g. "Material*" → Pointer64 + refId
|
||||
if (node.kind != NodeKind::Pointer64)
|
||||
if (nodeKind != NodeKind::Pointer64)
|
||||
changeNodeKind(nodeIdx, NodeKind::Pointer64);
|
||||
int idx = m_doc->tree.indexOfId(node.id);
|
||||
int idx = m_doc->tree.indexOfId(nodeId);
|
||||
if (idx >= 0 && m_doc->tree.nodes[idx].refId != entry.structId)
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||
|
||||
} else if (spec.arrayCount > 0) {
|
||||
// Array modifier: e.g. "Material[10]" → Array + Struct element
|
||||
if (node.kind != NodeKind::Array)
|
||||
if (nodeKind != NodeKind::Array)
|
||||
changeNodeKind(nodeIdx, NodeKind::Array);
|
||||
int idx = m_doc->tree.indexOfId(node.id);
|
||||
int idx = m_doc->tree.indexOfId(nodeId);
|
||||
if (idx >= 0) {
|
||||
auto& n = m_doc->tree.nodes[idx];
|
||||
if (n.elementKind != NodeKind::Struct || n.arrayLen != spec.arrayCount)
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeArrayMeta{node.id, n.elementKind, NodeKind::Struct,
|
||||
cmd::ChangeArrayMeta{nodeId, n.elementKind, NodeKind::Struct,
|
||||
n.arrayLen, spec.arrayCount}));
|
||||
if (n.refId != entry.structId)
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangePointerRef{node.id, n.refId, entry.structId}));
|
||||
cmd::ChangePointerRef{nodeId, n.refId, entry.structId}));
|
||||
}
|
||||
|
||||
} else {
|
||||
// Plain struct: e.g. "Material" → Struct + structTypeName + refId + collapsed
|
||||
if (node.kind != NodeKind::Struct)
|
||||
if (nodeKind != NodeKind::Struct)
|
||||
changeNodeKind(nodeIdx, NodeKind::Struct);
|
||||
int idx = m_doc->tree.indexOfId(node.id);
|
||||
int idx = m_doc->tree.indexOfId(nodeId);
|
||||
if (idx >= 0) {
|
||||
int refIdx = m_doc->tree.indexOfId(entry.structId);
|
||||
QString targetName;
|
||||
@@ -1789,11 +1890,11 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
||||
QString oldTypeName = m_doc->tree.nodes[idx].structTypeName;
|
||||
if (oldTypeName != targetName)
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeStructTypeName{node.id, oldTypeName, targetName}));
|
||||
cmd::ChangeStructTypeName{nodeId, oldTypeName, targetName}));
|
||||
// Set refId so compose can expand the referenced struct's children
|
||||
if (m_doc->tree.nodes[idx].refId != entry.structId)
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangePointerRef{node.id, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||
cmd::ChangePointerRef{nodeId, m_doc->tree.nodes[idx].refId, entry.structId}));
|
||||
// ChangePointerRef auto-sets collapsed=true when refId != 0
|
||||
}
|
||||
}
|
||||
@@ -1804,33 +1905,32 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
|
||||
}
|
||||
} else if (mode == TypePopupMode::ArrayElement) {
|
||||
if (entry.entryKind == TypeEntry::Primitive) {
|
||||
if (entry.primitiveKind != node.elementKind) {
|
||||
if (entry.primitiveKind != elemKind) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeArrayMeta{node.id,
|
||||
node.elementKind, entry.primitiveKind,
|
||||
node.arrayLen, node.arrayLen}));
|
||||
cmd::ChangeArrayMeta{nodeId,
|
||||
elemKind, entry.primitiveKind,
|
||||
arrLen, arrLen}));
|
||||
}
|
||||
} else if (entry.entryKind == TypeEntry::Composite) {
|
||||
if (node.elementKind != NodeKind::Struct || node.refId != entry.structId) {
|
||||
if (elemKind != NodeKind::Struct || nodeRefId != entry.structId) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeArrayMeta{node.id,
|
||||
node.elementKind, NodeKind::Struct,
|
||||
node.arrayLen, node.arrayLen}));
|
||||
if (node.refId != entry.structId) {
|
||||
cmd::ChangeArrayMeta{nodeId,
|
||||
elemKind, NodeKind::Struct,
|
||||
arrLen, arrLen}));
|
||||
if (nodeRefId != entry.structId) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangePointerRef{node.id, node.refId, entry.structId}));
|
||||
cmd::ChangePointerRef{nodeId, nodeRefId, entry.structId}));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mode == TypePopupMode::PointerTarget) {
|
||||
// "void" entry → refId 0; composite entry → real structId
|
||||
uint64_t realRefId = (entry.entryKind == TypeEntry::Composite) ? entry.structId : 0;
|
||||
if (realRefId != node.refId) {
|
||||
if (realRefId != nodeRefId) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangePointerRef{node.id, node.refId, realRefId}));
|
||||
cmd::ChangePointerRef{nodeId, nodeRefId, realRefId}));
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::attachViaPlugin(const QString& providerIdentifier, const QString& target) {
|
||||
@@ -1913,11 +2013,11 @@ void RcxController::setupAutoRefresh() {
|
||||
void RcxController::collectPointerRanges(
|
||||
uint64_t structId, uint64_t memBase,
|
||||
int depth, int maxDepth,
|
||||
QSet<uint64_t>& visited,
|
||||
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const
|
||||
{
|
||||
if (depth >= maxDepth) return;
|
||||
uint64_t key = memBase ^ (structId * 0x9E3779B97F4A7C15ULL);
|
||||
QPair<uint64_t,uint64_t> key{structId, memBase};
|
||||
if (visited.contains(key)) return;
|
||||
visited.insert(key);
|
||||
|
||||
@@ -1974,11 +2074,11 @@ void RcxController::onRefreshTick() {
|
||||
ranges.append({0, extent});
|
||||
|
||||
if (m_snapshotProv) {
|
||||
QSet<uint64_t> visited;
|
||||
QSet<QPair<uint64_t,uint64_t>> visited;
|
||||
uint64_t rootId = m_viewRootId;
|
||||
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
|
||||
rootId = m_doc->tree.nodes[0].id;
|
||||
collectPointerRanges(rootId, 0, 0, 4, visited, ranges);
|
||||
collectPointerRanges(rootId, 0, 0, 99, visited, ranges);
|
||||
}
|
||||
|
||||
m_readInFlight = true;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QUndoCommand>
|
||||
#include <QTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
@@ -138,7 +139,7 @@ private:
|
||||
int m_activeSourceIdx = -1;
|
||||
|
||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||
TypeSelectorPopup* m_cachedPopup = nullptr;
|
||||
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
@@ -169,7 +170,7 @@ private:
|
||||
void resetSnapshot();
|
||||
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
||||
int depth, int maxDepth,
|
||||
QSet<uint64_t>& visited,
|
||||
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||
};
|
||||
|
||||
|
||||
32
src/core.h
32
src/core.h
@@ -27,7 +27,6 @@ enum class NodeKind : uint8_t {
|
||||
Pointer32, Pointer64,
|
||||
Vec2, Vec3, Vec4, Mat4x4,
|
||||
UTF8, UTF16,
|
||||
Padding,
|
||||
Struct, Array
|
||||
};
|
||||
|
||||
@@ -37,11 +36,11 @@ inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast
|
||||
#endif
|
||||
namespace rcx { // reopen
|
||||
|
||||
// ── Kind flags (replaces repeated Hex/Padding switches) ──
|
||||
// ── Kind flags (replaces repeated Hex switches) ──
|
||||
|
||||
enum KindFlags : uint32_t {
|
||||
KF_None = 0,
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout)
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
|
||||
KF_Container = 1 << 1, // Struct/Array
|
||||
KF_String = 1 << 2, // UTF8/UTF16
|
||||
KF_Vector = 1 << 3, // Vec2/3/4
|
||||
@@ -84,7 +83,6 @@ inline constexpr KindMeta kKindMeta[] = {
|
||||
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
|
||||
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
|
||||
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
|
||||
{NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
|
||||
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||
};
|
||||
@@ -155,7 +153,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
|
||||
enum Marker : int {
|
||||
M_CONT = 0,
|
||||
M_PAD = 1,
|
||||
M_PTR0 = 2,
|
||||
M_CYCLE = 3,
|
||||
M_ERR = 4,
|
||||
@@ -187,9 +184,12 @@ struct Node {
|
||||
int byteSize() const {
|
||||
switch (kind) {
|
||||
case NodeKind::UTF8: return strLen;
|
||||
case NodeKind::UTF16: return strLen * 2;
|
||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
||||
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
||||
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(elementKind);
|
||||
if (elemSz <= 0) return 0;
|
||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||
}
|
||||
default: return sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
@@ -221,8 +221,8 @@ struct Node {
|
||||
n.classKeyword = o["classKeyword"].toString();
|
||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.arrayLen = o["arrayLen"].toInt(1);
|
||||
n.strLen = o["strLen"].toInt(64);
|
||||
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.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
@@ -535,7 +535,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Hex/Padding: ASCII preview occupies the name column (padded to nameW)
|
||||
// Hex: ASCII preview occupies the name column (padded to nameW)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
return {start, start + nameW, true};
|
||||
|
||||
@@ -547,9 +547,9 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex/Padding uses nameW for ASCII column (same as regular name column)
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
// Hex uses nameW for ASCII column (same as regular name column)
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
|
||||
@@ -567,8 +567,8 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
int start;
|
||||
|
||||
@@ -141,7 +141,7 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
||||
|
||||
// Hex/Padding node dim indicator — overrides text color
|
||||
// Hex node dim indicator — overrides text color
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_HEX_DIM, 17 /*INDIC_TEXTFORE*/);
|
||||
|
||||
@@ -241,9 +241,6 @@ void RcxEditor::setupMarkers() {
|
||||
// M_CONT (0): continuation line (metadata only, no visual)
|
||||
m_sci->markerDefine(QsciScintilla::Invisible, M_CONT);
|
||||
|
||||
// M_PAD (1): padding line (metadata only, no visual)
|
||||
m_sci->markerDefine(QsciScintilla::Invisible, M_PAD);
|
||||
|
||||
// M_PTR0 (2): right triangle
|
||||
m_sci->markerDefine(QsciScintilla::RightTriangle, M_PTR0);
|
||||
|
||||
@@ -1038,9 +1035,6 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
|
||||
|
||||
if (lm->nodeIdx < 0) return false;
|
||||
|
||||
// Padding: reject value editing (hex bytes are display-only)
|
||||
if (t == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
return false;
|
||||
@@ -1221,9 +1215,6 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Padding nodes: hex bytes are display-only, not editable
|
||||
if (outTarget == EditTarget::Value && lm.nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((outTarget == EditTarget::Name || outTarget == EditTarget::Value) && isHexNode(lm.nodeKind))
|
||||
return false;
|
||||
@@ -1330,7 +1321,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
// Single-click on editable token of already-selected node → edit
|
||||
int tLine, tCol; EditTarget t;
|
||||
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
|
||||
if (alreadySelected && plain) {
|
||||
// Type/ArrayElementType/PointerTarget open a dismissible popup
|
||||
// (not inline text edit), so allow on first click without
|
||||
// requiring the node to be pre-selected.
|
||||
bool isPopupTarget = (t == EditTarget::Type
|
||||
|| t == EditTarget::ArrayElementType
|
||||
|| t == EditTarget::PointerTarget);
|
||||
if ((alreadySelected || isPopupTarget) && plain) {
|
||||
if (!alreadySelected)
|
||||
emit nodeClicked(h.line, h.nodeId, me->modifiers());
|
||||
m_pendingClickNodeId = 0;
|
||||
return beginInlineEdit(t, tLine, tCol);
|
||||
}
|
||||
@@ -1673,9 +1672,6 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
(target == EditTarget::BaseAddress || target == EditTarget::Source
|
||||
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
|
||||
return false;
|
||||
// Padding: reject value editing (display-only hex bytes)
|
||||
if (target == EditTarget::Value && lm->nodeKind == NodeKind::Padding)
|
||||
return false;
|
||||
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only)
|
||||
if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind))
|
||||
return false;
|
||||
|
||||
@@ -293,7 +293,6 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
line += QStringLiteral("]");
|
||||
return line;
|
||||
}
|
||||
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
||||
case NodeKind::UTF8: {
|
||||
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
||||
int end = bytes.indexOf('\0');
|
||||
@@ -344,21 +343,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Hex nodes and Padding: hex byte preview (ASCII padded to colName to align with value column)
|
||||
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes).leftJustified(colName, ' ');
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
// Hex8..Hex64: single line, ASCII padded to colName so hex column aligns with value column
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
|
||||
@@ -50,7 +50,6 @@ static QString cTypeName(NodeKind kind) {
|
||||
case NodeKind::Mat4x4: return QStringLiteral("float");
|
||||
case NodeKind::UTF8: return QStringLiteral("char");
|
||||
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
||||
case NodeKind::Padding: return QStringLiteral("uint8_t");
|
||||
default: return QStringLiteral("uint8_t");
|
||||
}
|
||||
}
|
||||
@@ -123,8 +122,6 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||
case NodeKind::UTF16:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||
case NodeKind::Padding:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc;
|
||||
case NodeKind::Pointer32: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
@@ -169,7 +166,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
auto emitPadRun = [&](int offset, int size) {
|
||||
if (size <= 0) return;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
||||
.arg(ctx.cType(NodeKind::Padding))
|
||||
.arg(QStringLiteral("uint8_t"))
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(size, 16).toUpper())
|
||||
.arg(offsetComment(offset));
|
||||
|
||||
232
src/main.cpp
232
src/main.cpp
@@ -43,6 +43,7 @@
|
||||
#include <QDesktopServices>
|
||||
#include "themes/thememanager.h"
|
||||
#include "themes/themeeditor.h"
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -141,7 +142,9 @@ public:
|
||||
if ((w->windowFlags() & Qt::Window) == Qt::Window
|
||||
&& !w->property("DarkTitleBar").toBool()) {
|
||||
w->setProperty("DarkTitleBar", true);
|
||||
#ifdef _WIN32
|
||||
setDarkTitleBar(w);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return QApplication::notify(receiver, event);
|
||||
@@ -160,6 +163,13 @@ public:
|
||||
s = QSize(s.width() + 24, s.height() + 4);
|
||||
return s;
|
||||
}
|
||||
int pixelMetric(PixelMetric metric, const QStyleOption* opt,
|
||||
const QWidget* w) const override {
|
||||
// Kill the 1px frame margin Fusion reserves around QMenu contents
|
||||
if (metric == PM_MenuPanelWidth)
|
||||
return 0;
|
||||
return QProxyStyle::pixelMetric(metric, opt, w);
|
||||
}
|
||||
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||
QPainter* p, const QWidget* w) const override {
|
||||
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
|
||||
@@ -205,7 +215,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.background);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
@@ -301,6 +311,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
createMenus();
|
||||
createStatusBar();
|
||||
|
||||
// Restore menu bar title case setting (after menus are created)
|
||||
{
|
||||
bool titleCase = QSettings("Reclass", "Reclass").value("menuBarTitleCase", true).toBool();
|
||||
m_titleBar->setMenuBarTitleCase(titleCase);
|
||||
}
|
||||
|
||||
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
|
||||
|
||||
@@ -310,9 +325,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
// Load plugins
|
||||
m_pluginManager.LoadPlugins();
|
||||
|
||||
// MCP bridge (on by default)
|
||||
// Start MCP bridge
|
||||
m_mcp = new McpBridge(this, this);
|
||||
m_mcp->start();
|
||||
if (QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool())
|
||||
m_mcp->start();
|
||||
|
||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
||||
this, [this](QMdiSubWindow*) {
|
||||
@@ -341,23 +357,25 @@ QIcon MainWindow::makeIcon(const QString& svgPath) {
|
||||
void MainWindow::createMenus() {
|
||||
// File
|
||||
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
||||
file->addAction("&New", this, &MainWindow::newDocument, QKeySequence::New);
|
||||
file->addAction("New &Tab", this, &MainWindow::newFile, QKeySequence(Qt::CTRL | Qt::Key_T));
|
||||
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", this, &MainWindow::openFile, QKeySequence::Open);
|
||||
file->addAction("&New", QKeySequence::New, this, &MainWindow::newDocument);
|
||||
file->addAction("New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), this, &MainWindow::newFile);
|
||||
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", QKeySequence::Open, this, &MainWindow::openFile);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", this, &MainWindow::saveFile, QKeySequence::Save);
|
||||
file->addAction(makeIcon(":/vsicons/save-as.svg"), "Save &As...", this, &MainWindow::saveFileAs, QKeySequence::SaveAs);
|
||||
file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile);
|
||||
file->addAction(makeIcon(":/vsicons/save-as.svg"), "Save &As...", QKeySequence::SaveAs, this, &MainWindow::saveFileAs);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp);
|
||||
file->addSeparator();
|
||||
m_mcpAction = file->addAction("Stop &MCP Server", this, &MainWindow::toggleMcp);
|
||||
m_mcpAction = file->addAction(QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server", this, &MainWindow::toggleMcp);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
|
||||
file->addAction(makeIcon(":/vsicons/settings-gear.svg"), "&Options...", this, &MainWindow::showOptionsDialog);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", QKeySequence(Qt::Key_Close), this, &QMainWindow::close);
|
||||
|
||||
// Edit
|
||||
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", this, &MainWindow::undo, QKeySequence::Undo);
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", this, &MainWindow::redo, QKeySequence::Redo);
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", QKeySequence::Undo, this, &MainWindow::undo);
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo);
|
||||
edit->addSeparator();
|
||||
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
||||
|
||||
@@ -415,10 +433,10 @@ void MainWindow::createMenus() {
|
||||
|
||||
// Node
|
||||
auto* node = m_titleBar->menuBar()->addMenu("&Node");
|
||||
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", this, &MainWindow::addNode, QKeySequence(Qt::Key_Insert));
|
||||
node->addAction(makeIcon(":/vsicons/remove.svg"), "&Remove Field", this, &MainWindow::removeNode, QKeySequence::Delete);
|
||||
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", this, &MainWindow::changeNodeType, QKeySequence(Qt::Key_T));
|
||||
node->addAction(makeIcon(":/vsicons/edit.svg"), "Re&name", this, &MainWindow::renameNodeAction, QKeySequence(Qt::Key_F2));
|
||||
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", QKeySequence(Qt::Key_Insert), this, &MainWindow::addNode);
|
||||
node->addAction(makeIcon(":/vsicons/remove.svg"), "&Remove Field", QKeySequence::Delete, this, &MainWindow::removeNode);
|
||||
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", QKeySequence(Qt::Key_T), this, &MainWindow::changeNodeType);
|
||||
node->addAction(makeIcon(":/vsicons/edit.svg"), "Re&name", QKeySequence(Qt::Key_F2), this, &MainWindow::renameNodeAction);
|
||||
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
||||
|
||||
// Plugins
|
||||
@@ -665,6 +683,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
}
|
||||
updateWindowTitle();
|
||||
rebuildWorkspaceModel();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -737,6 +756,25 @@ static void buildBallDemo(NodeTree& tree) {
|
||||
|
||||
// Material[2] materials at offset 128 (112 + 16 for float[4])
|
||||
{ Node n; n.kind = NodeKind::Array; n.name = "materials"; n.parentId = ballId; n.offset = 128; n.elementKind = NodeKind::Struct; n.arrayLen = 2; n.refId = matId; tree.addNode(n); }
|
||||
|
||||
// Unnamed struct (128 bytes of hex64 fields)
|
||||
Node unnamed;
|
||||
unnamed.kind = NodeKind::Struct;
|
||||
unnamed.name = "instance";
|
||||
unnamed.structTypeName = "Unnamed";
|
||||
unnamed.parentId = 0;
|
||||
unnamed.offset = 0;
|
||||
int ui = tree.addNode(unnamed);
|
||||
uint64_t unnamedId = tree.nodes[ui].id;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
n.parentId = unnamedId;
|
||||
n.offset = i * 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::newFile() {
|
||||
@@ -785,41 +823,7 @@ void MainWindow::newDocument() {
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
// Tab 1: Ball demo
|
||||
project_new();
|
||||
|
||||
// Tab 2: Unnamed struct with hex64 fields
|
||||
{
|
||||
auto* doc = new RcxDocument(this);
|
||||
QByteArray data(256, '\0');
|
||||
doc->loadData(data);
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
|
||||
Node s;
|
||||
s.kind = NodeKind::Struct;
|
||||
s.name = "instance";
|
||||
s.structTypeName = "Unnamed";
|
||||
s.parentId = 0;
|
||||
s.offset = 0;
|
||||
int si = doc->tree.addNode(s);
|
||||
uint64_t sId = doc->tree.nodes[si].id;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
n.parentId = sId;
|
||||
n.offset = i * 8;
|
||||
doc->tree.addNode(n);
|
||||
}
|
||||
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
|
||||
// Focus Ball tab
|
||||
if (auto* first = m_mdiArea->subWindowList().value(0))
|
||||
m_mdiArea->setActiveSubWindow(first);
|
||||
}
|
||||
|
||||
void MainWindow::openFile() {
|
||||
@@ -1005,6 +1009,13 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
statusBar()->setPalette(sbPal);
|
||||
}
|
||||
|
||||
// Workspace tree: text color matches menu bar
|
||||
if (m_workspaceTree) {
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, theme.textDim);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
|
||||
// Split pane tab widgets
|
||||
for (auto& state : m_tabs) {
|
||||
for (auto& pane : state.panes) {
|
||||
@@ -1024,6 +1035,39 @@ void MainWindow::editTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: when adding more and more options, this func becomes very clunky. Fix
|
||||
void MainWindow::showOptionsDialog() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
OptionsResult current;
|
||||
current.themeIndex = tm.currentIndex();
|
||||
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
|
||||
|
||||
OptionsDialog dlg(current, this);
|
||||
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
||||
|
||||
auto r = dlg.result();
|
||||
|
||||
if (r.themeIndex != current.themeIndex)
|
||||
tm.setCurrent(r.themeIndex);
|
||||
|
||||
if (r.fontName != current.fontName)
|
||||
setEditorFont(r.fontName);
|
||||
|
||||
if (r.menuBarTitleCase != current.menuBarTitleCase) {
|
||||
m_titleBar->setMenuBarTitleCase(r.menuBarTitleCase);
|
||||
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
||||
}
|
||||
|
||||
if (r.safeMode != current.safeMode)
|
||||
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
|
||||
|
||||
if (r.autoStartMcp != current.autoStartMcp)
|
||||
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
|
||||
}
|
||||
|
||||
void MainWindow::setEditorFont(const QString& fontName) {
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
settings.setValue("font", fontName);
|
||||
@@ -1380,7 +1424,7 @@ void MainWindow::project_close(QMdiSubWindow* sub) {
|
||||
// ── Workspace Dock ──
|
||||
|
||||
void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceDock = new QDockWidget("Workspace", this);
|
||||
m_workspaceDock = new QDockWidget("Project Tree", this);
|
||||
m_workspaceDock->setObjectName("WorkspaceDock");
|
||||
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
|
||||
@@ -1390,81 +1434,51 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setModel(m_workspaceModel);
|
||||
m_workspaceTree->setHeaderHidden(true);
|
||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
|
||||
// Match editor font
|
||||
{
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
m_workspaceTree->setFont(f);
|
||||
}
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
|
||||
m_workspaceDock->setWidget(m_workspaceTree);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||
m_workspaceDock->hide();
|
||||
|
||||
|
||||
connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) {
|
||||
// Data roles: UserRole=QMdiSubWindow*, UserRole+1=structId, UserRole+2=nodeId
|
||||
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* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
|
||||
if (!sub || !m_tabs.contains(sub)) return;
|
||||
|
||||
m_mdiArea->setActiveSubWindow(sub);
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
auto nodeIdVar = index.data(Qt::UserRole + 2);
|
||||
|
||||
if (structIdVar.isValid()) {
|
||||
// Double-clicked a struct: set as view root
|
||||
uint64_t structId = structIdVar.toULongLong();
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
} else if (nodeIdVar.isValid()) {
|
||||
// Double-clicked a field: find its root struct, set as view root, scroll to field
|
||||
uint64_t nodeId = nodeIdVar.toULongLong();
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
// Walk up to find root struct
|
||||
uint64_t rootId = 0;
|
||||
uint64_t cur = nodeId;
|
||||
while (cur != 0) {
|
||||
int idx = tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
if (tree.nodes[idx].parentId == 0) { rootId = cur; break; }
|
||||
cur = tree.nodes[idx].parentId;
|
||||
}
|
||||
if (rootId != 0) {
|
||||
int ri = tree.indexOfId(rootId);
|
||||
if (ri >= 0) tree.nodes[ri].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(rootId);
|
||||
}
|
||||
m_tabs[sub].ctrl->scrollToNodeId(nodeId);
|
||||
} else if (!index.parent().isValid()) {
|
||||
// Double-clicked project root: clear view root to show all
|
||||
m_tabs[sub].ctrl->setViewRootId(0);
|
||||
}
|
||||
// Type/Enum node: navigate to it
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::rebuildWorkspaceModel() {
|
||||
m_workspaceModel->clear();
|
||||
|
||||
auto* sub = m_mdiArea->activeSubWindow();
|
||||
if (!sub || !m_tabs.contains(sub)) return;
|
||||
|
||||
TabState& tab = m_tabs[sub];
|
||||
QString tabName = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
|
||||
buildWorkspaceModel(m_workspaceModel, tab.doc->tree, tabName,
|
||||
static_cast<void*>(sub));
|
||||
m_workspaceTree->expandAll();
|
||||
QVector<rcx::TabInfo> tabs;
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||
TabState& tab = it.value();
|
||||
QString name = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||
}
|
||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
||||
m_workspaceTree->expandToDepth(1);
|
||||
}
|
||||
|
||||
void MainWindow::showPluginsDialog() {
|
||||
|
||||
@@ -49,6 +49,7 @@ private slots:
|
||||
void exportCpp();
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
|
||||
public:
|
||||
// Project Lifecycle API
|
||||
|
||||
@@ -248,7 +248,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
|
||||
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
|
||||
"Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Padding Struct Array"},
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
|
||||
327
src/optionsdialog.cpp
Normal file
327
src/optionsdialog.cpp
Normal file
@@ -0,0 +1,327 @@
|
||||
#include "optionsdialog.h"
|
||||
#include "themes/thememanager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QGraphicsDropShadowEffect>
|
||||
#include <QEvent>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Options");
|
||||
setFixedSize(700, 450);
|
||||
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(8);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
|
||||
// -- Middle: left column (search + tree) | right column (pages) --
|
||||
auto* middleLayout = new QHBoxLayout;
|
||||
middleLayout->setSpacing(8);
|
||||
|
||||
// Left column: search bar + tree
|
||||
auto* leftColumn = new QVBoxLayout;
|
||||
leftColumn->setSpacing(4);
|
||||
|
||||
m_search = new QLineEdit;
|
||||
m_search->setPlaceholderText("Search Options (Ctrl+E)");
|
||||
m_search->setClearButtonEnabled(true);
|
||||
connect(m_search, &QLineEdit::textChanged, this, &OptionsDialog::filterTree);
|
||||
leftColumn->addWidget(m_search);
|
||||
|
||||
m_tree = new QTreeWidget;
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setFixedWidth(200);
|
||||
|
||||
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
|
||||
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
||||
m_tree->expandAll();
|
||||
m_tree->setCurrentItem(generalItem);
|
||||
leftColumn->addWidget(m_tree, 1);
|
||||
|
||||
middleLayout->addLayout(leftColumn);
|
||||
|
||||
// Right column: stacked pages with group boxes
|
||||
m_pages = new QStackedWidget;
|
||||
|
||||
// -- General page --
|
||||
auto* generalPage = new QWidget;
|
||||
auto* generalLayout = new QVBoxLayout(generalPage);
|
||||
generalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generalLayout->setSpacing(8);
|
||||
|
||||
// Visual Experience group box
|
||||
auto* visualGroup = new QGroupBox("Visual Experience");
|
||||
auto* visualLayout = new QFormLayout(visualGroup);
|
||||
visualLayout->setSpacing(8);
|
||||
visualLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_themeCombo = new QComboBox;
|
||||
auto& tm = ThemeManager::instance();
|
||||
for (const auto& theme : tm.themes())
|
||||
m_themeCombo->addItem(theme.name);
|
||||
m_themeCombo->setCurrentIndex(current.themeIndex);
|
||||
m_themeCombo->setObjectName("themeCombo");
|
||||
visualLayout->addRow("Color theme:", m_themeCombo);
|
||||
|
||||
m_fontCombo = new QComboBox;
|
||||
m_fontCombo->addItem("JetBrains Mono");
|
||||
m_fontCombo->addItem("Consolas");
|
||||
m_fontCombo->setCurrentText(current.fontName);
|
||||
m_fontCombo->setObjectName("fontCombo");
|
||||
visualLayout->addRow("Editor Font:", m_fontCombo);
|
||||
|
||||
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
|
||||
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||
visualLayout->addRow(m_titleCaseCheck);
|
||||
|
||||
generalLayout->addWidget(visualGroup);
|
||||
|
||||
// Safe Mode group box
|
||||
auto* safeModeGroup = new QGroupBox("Preview Features");
|
||||
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
|
||||
safeModeLayout->setSpacing(4);
|
||||
|
||||
m_safeModeCheck = new QCheckBox("Safe Mode");
|
||||
m_safeModeCheck->setChecked(current.safeMode);
|
||||
m_safeModeCheck->setStyleSheet(QStringLiteral(
|
||||
"QCheckBox { font-weight: bold; }"));
|
||||
safeModeLayout->addWidget(m_safeModeCheck);
|
||||
|
||||
auto* safeModeDesc = new QLabel(
|
||||
"Enable to use the default OS icon for this application and "
|
||||
"create the window with the name of the executable file.");
|
||||
safeModeDesc->setWordWrap(true);
|
||||
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
|
||||
safeModeLayout->addWidget(safeModeDesc);
|
||||
|
||||
generalLayout->addWidget(safeModeGroup);
|
||||
generalLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generalPage); // index 0
|
||||
m_pageKeywords[generalItem] = collectPageKeywords(generalPage);
|
||||
|
||||
// -- AI Features page --
|
||||
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
||||
|
||||
auto* aiPage = new QWidget;
|
||||
auto* aiLayout = new QVBoxLayout(aiPage);
|
||||
aiLayout->setContentsMargins(0, 0, 0, 0);
|
||||
aiLayout->setSpacing(8);
|
||||
|
||||
auto* mcpGroup = new QGroupBox("MCP Server");
|
||||
auto* mcpLayout = new QVBoxLayout(mcpGroup);
|
||||
mcpLayout->setSpacing(4);
|
||||
|
||||
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
|
||||
m_autoMcpCheck->setChecked(current.autoStartMcp);
|
||||
m_autoMcpCheck->setStyleSheet(QStringLiteral(
|
||||
"QCheckBox { font-weight: bold; }"));
|
||||
mcpLayout->addWidget(m_autoMcpCheck);
|
||||
|
||||
auto* mcpDesc = new QLabel(
|
||||
"Automatically start the MCP bridge server when the application launches, "
|
||||
"allowing external AI tools to connect and interact with the editor.");
|
||||
mcpDesc->setWordWrap(true);
|
||||
mcpDesc->setContentsMargins(20, 0, 0, 0);
|
||||
mcpLayout->addWidget(mcpDesc);
|
||||
|
||||
aiLayout->addWidget(mcpGroup);
|
||||
aiLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(aiPage); // index 1
|
||||
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
|
||||
|
||||
middleLayout->addWidget(m_pages, 1);
|
||||
|
||||
mainLayout->addLayout(middleLayout, 1);
|
||||
|
||||
// Tree <-> page connection
|
||||
m_itemPageIndex[generalItem] = 0;
|
||||
m_itemPageIndex[aiItem] = 1;
|
||||
connect(m_tree, &QTreeWidget::currentItemChanged, this,
|
||||
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
|
||||
if (!item) return;
|
||||
auto it = m_itemPageIndex.find(item);
|
||||
if (it != m_itemPageIndex.end())
|
||||
m_pages->setCurrentIndex(it.value());
|
||||
});
|
||||
|
||||
// -- Button box --
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
// -- Styling --
|
||||
|
||||
// Combo boxes: set directly so the popup (top-level widget) inherits it
|
||||
QString comboStyle = QStringLiteral(
|
||||
"QComboBox {"
|
||||
" background: %1; color: %2; border: 1px solid %3;"
|
||||
" padding: 3px 8px; font-size: 12px;"
|
||||
"}"
|
||||
"QComboBox::drop-down {"
|
||||
" border: none; border-left: 1px solid %3;"
|
||||
" width: 20px;"
|
||||
"}"
|
||||
"QComboBox::down-arrow {"
|
||||
" image: url(:/vsicons/chevron-down.svg);"
|
||||
" width: 12px; height: 12px;"
|
||||
"}"
|
||||
"QComboBox QAbstractItemView {"
|
||||
" background: %1; color: %2; border: 1px solid %3;"
|
||||
" selection-background-color: %4;"
|
||||
"}")
|
||||
.arg(t.backgroundAlt.name(), t.text.name(),
|
||||
t.border.name(), t.hover.name());
|
||||
m_themeCombo->setStyleSheet(comboStyle);
|
||||
m_fontCombo->setStyleSheet(comboStyle);
|
||||
|
||||
// Dialog-wide stylesheet for everything else
|
||||
setStyleSheet(QStringLiteral(
|
||||
"QDialog { background: %1; }"
|
||||
|
||||
"QLineEdit {"
|
||||
" background: %2; color: %3; border: 1px solid %4;"
|
||||
" padding: 4px 8px; font-size: 12px;"
|
||||
"}"
|
||||
|
||||
"QTreeWidget {"
|
||||
" background: %2; color: %3; border: 1px solid %4;"
|
||||
" font-size: 12px; outline: none;"
|
||||
"}"
|
||||
"QTreeWidget::item { padding: 3px 0; outline: none; }"
|
||||
"QTreeWidget::item:selected { background: %5; color: %3; }"
|
||||
"QTreeWidget::item:hover { background: %6; }"
|
||||
|
||||
"QGroupBox {"
|
||||
" color: %3; border: 1px solid %4;"
|
||||
" margin-top: 8px; padding: 12px 8px 8px 8px;"
|
||||
" font-size: 12px; font-weight: bold;"
|
||||
"}"
|
||||
"QGroupBox::title {"
|
||||
" subcontrol-origin: margin;"
|
||||
" left: 8px; padding: 0 4px;"
|
||||
"}"
|
||||
|
||||
"QLabel { color: %3; font-size: 12px; }"
|
||||
|
||||
"QCheckBox { color: %3; font-size: 12px; spacing: 6px; }"
|
||||
|
||||
"QPushButton {"
|
||||
" background: %2; color: %3; border: 1px solid %4;"
|
||||
" padding: 5px 16px; min-width: 70px; font-size: 12px;"
|
||||
" outline: none;"
|
||||
"}"
|
||||
"QPushButton:hover { background: %6; }"
|
||||
"QPushButton:pressed { background: %1; }"
|
||||
"QPushButton:focus { outline: none; }")
|
||||
.arg(t.background.name(), // %1
|
||||
t.backgroundAlt.name(), // %2
|
||||
t.text.name(), // %3
|
||||
t.border.name(), // %4
|
||||
t.selection.name(), // %5
|
||||
t.hover.name())); // %6
|
||||
|
||||
// Install hover shadow on interactive widgets (not buttons — they use stylesheet hover)
|
||||
for (auto* w : {static_cast<QWidget*>(m_search),
|
||||
static_cast<QWidget*>(m_themeCombo),
|
||||
static_cast<QWidget*>(m_fontCombo),
|
||||
static_cast<QWidget*>(m_titleCaseCheck),
|
||||
static_cast<QWidget*>(m_safeModeCheck),
|
||||
static_cast<QWidget*>(m_autoMcpCheck)})
|
||||
w->installEventFilter(this);
|
||||
|
||||
m_shadowColor = t.text;
|
||||
m_shadowColor.setAlpha(80);
|
||||
}
|
||||
|
||||
bool OptionsDialog::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (event->type() == QEvent::Enter) {
|
||||
auto* w = qobject_cast<QWidget*>(obj);
|
||||
if (w && !w->graphicsEffect()) {
|
||||
auto* shadow = new QGraphicsDropShadowEffect(w);
|
||||
shadow->setBlurRadius(12);
|
||||
shadow->setOffset(0, 0);
|
||||
shadow->setColor(m_shadowColor);
|
||||
w->setGraphicsEffect(shadow);
|
||||
}
|
||||
} else if (event->type() == QEvent::Leave) {
|
||||
auto* w = qobject_cast<QWidget*>(obj);
|
||||
if (w)
|
||||
w->setGraphicsEffect(nullptr);
|
||||
}
|
||||
return QDialog::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
OptionsResult OptionsDialog::result() const {
|
||||
OptionsResult r;
|
||||
r.themeIndex = m_themeCombo->currentIndex();
|
||||
r.fontName = m_fontCombo->currentText();
|
||||
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
||||
r.safeMode = m_safeModeCheck->isChecked();
|
||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||
return r;
|
||||
}
|
||||
|
||||
QStringList OptionsDialog::collectPageKeywords(QWidget* page) {
|
||||
QStringList keywords;
|
||||
for (auto* child : page->findChildren<QWidget*>()) {
|
||||
if (auto* label = qobject_cast<QLabel*>(child))
|
||||
keywords << label->text();
|
||||
else if (auto* cb = qobject_cast<QCheckBox*>(child))
|
||||
keywords << cb->text();
|
||||
else if (auto* gb = qobject_cast<QGroupBox*>(child))
|
||||
keywords << gb->title();
|
||||
else if (auto* combo = qobject_cast<QComboBox*>(child)) {
|
||||
for (int i = 0; i < combo->count(); ++i)
|
||||
keywords << combo->itemText(i);
|
||||
}
|
||||
}
|
||||
return keywords;
|
||||
}
|
||||
|
||||
void OptionsDialog::filterTree(const QString& text) {
|
||||
std::function<bool(QTreeWidgetItem*)> filter = [&](QTreeWidgetItem* item) -> bool {
|
||||
bool anyChildVisible = false;
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
if (filter(item->child(i)))
|
||||
anyChildVisible = true;
|
||||
}
|
||||
|
||||
bool selfMatch = item->text(0).contains(text, Qt::CaseInsensitive);
|
||||
if (!selfMatch) {
|
||||
for (const auto& kw : m_pageKeywords.value(item)) {
|
||||
if (kw.contains(text, Qt::CaseInsensitive)) {
|
||||
selfMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool visible = selfMatch || anyChildVisible;
|
||||
item->setHidden(!visible);
|
||||
|
||||
if (visible && item->childCount() > 0)
|
||||
item->setExpanded(true);
|
||||
|
||||
return visible;
|
||||
};
|
||||
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i)
|
||||
filter(m_tree->topLevelItem(i));
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
53
src/optionsdialog.h
Normal file
53
src/optionsdialog.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "themes/theme.h"
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTreeWidget>
|
||||
#include <QStackedWidget>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QHash>
|
||||
#include <QColor>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct OptionsResult {
|
||||
int themeIndex = 0;
|
||||
QString fontName;
|
||||
bool menuBarTitleCase = true;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = false;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
|
||||
|
||||
OptionsResult result() const;
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
private:
|
||||
void filterTree(const QString& text);
|
||||
static QStringList collectPageKeywords(QWidget* page);
|
||||
|
||||
QLineEdit* m_search = nullptr;
|
||||
QTreeWidget* m_tree = nullptr;
|
||||
QStackedWidget* m_pages = nullptr;
|
||||
QComboBox* m_themeCombo = nullptr;
|
||||
QComboBox* m_fontCombo = nullptr;
|
||||
QCheckBox* m_titleCaseCheck = nullptr;
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
|
||||
QColor m_shadowColor;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
// tree item → stacked widget page index
|
||||
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -47,5 +47,9 @@
|
||||
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
||||
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
|
||||
<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="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#585858",
|
||||
"textFaint": "#505050",
|
||||
"hover": "#2b2b2b",
|
||||
"selected": "#232323",
|
||||
"hover": "#1e1e1e",
|
||||
"selected": "#1e1e1e",
|
||||
"selection": "#2b2b2b",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#636369",
|
||||
"textFaint": "#4d4d55",
|
||||
"hover": "#3e3e42",
|
||||
"selected": "#2d2d30",
|
||||
"hover": "#2c2c2f",
|
||||
"selected": "#262629",
|
||||
"selection": "#264f78",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#7a7a6e",
|
||||
"textMuted": "#555550",
|
||||
"textFaint": "#464646",
|
||||
"hover": "#373737",
|
||||
"selected": "#2d2d2d",
|
||||
"hover": "#282828",
|
||||
"selected": "#262626",
|
||||
"selection": "#21213A",
|
||||
"syntaxKeyword": "#AA9565",
|
||||
"syntaxNumber": "#AAA98C",
|
||||
|
||||
@@ -114,6 +114,33 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
||||
m_titleCase = titleCase;
|
||||
for (QAction* action : m_menuBar->actions()) {
|
||||
QString text = action->text();
|
||||
QString clean = text;
|
||||
clean.remove('&');
|
||||
|
||||
if (titleCase) {
|
||||
QString result;
|
||||
bool capitalizeNext = true;
|
||||
for (int i = 0; i < clean.length(); ++i) {
|
||||
QChar ch = clean[i];
|
||||
if (ch.isLetter()) {
|
||||
result += capitalizeNext ? ch.toUpper() : ch.toLower();
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result += ch;
|
||||
if (ch.isSpace()) capitalizeNext = true;
|
||||
}
|
||||
}
|
||||
action->setText("&" + result);
|
||||
} else {
|
||||
action->setText("&" + clean.toUpper());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::updateMaximizeIcon() {
|
||||
if (window()->isMaximized())
|
||||
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));
|
||||
|
||||
@@ -16,6 +16,8 @@ public:
|
||||
QMenuBar* menuBar() const { return m_menuBar; }
|
||||
void applyTheme(const Theme& theme);
|
||||
void setShowIcon(bool show);
|
||||
void setMenuBarTitleCase(bool titleCase);
|
||||
bool menuBarTitleCase() const { return m_titleCase; }
|
||||
|
||||
void updateMaximizeIcon();
|
||||
|
||||
@@ -32,6 +34,7 @@ private:
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = true;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QIntValidator>
|
||||
#include <QElapsedTimer>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
@@ -121,7 +122,7 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// 18px gutter: side triangle if current
|
||||
// Gutter: side triangle if current
|
||||
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
||||
const TypeEntry& entry = (*m_filtered)[row];
|
||||
bool isCurrent = false;
|
||||
@@ -130,13 +131,13 @@ public:
|
||||
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||
isCurrent = (entry.structId == m_current->structId);
|
||||
if (isCurrent) {
|
||||
painter->setPen(t.syntaxType);
|
||||
painter->setPen(t.text);
|
||||
painter->setFont(m_font);
|
||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||
painter->drawText(QRect(x, y, 10, h), Qt::AlignCenter,
|
||||
QString(QChar(0x25B8)));
|
||||
}
|
||||
}
|
||||
x += 18;
|
||||
x += 10;
|
||||
|
||||
// Icon 16x16 — only for composite entries
|
||||
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
@@ -368,6 +369,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setFrameShape(QFrame::NoFrame);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setMouseTracking(true);
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
@@ -384,10 +386,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::warmUp() {
|
||||
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
|
||||
// subsystem the first time a popup with complex children is shown. Pre-pay it
|
||||
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
|
||||
// ourselves.
|
||||
{
|
||||
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||
primer->resize(300, 400);
|
||||
auto* lay = new QVBoxLayout(primer);
|
||||
lay->addWidget(new QLabel(QStringLiteral("x")));
|
||||
lay->addWidget(new QLineEdit);
|
||||
auto* model = new QStringListModel(primer);
|
||||
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
|
||||
model->setStringList(items);
|
||||
auto* lv = new QListView;
|
||||
lv->setModel(model);
|
||||
lay->addWidget(lv);
|
||||
primer->show();
|
||||
QApplication::processEvents();
|
||||
primer->hide();
|
||||
QApplication::processEvents();
|
||||
delete primer;
|
||||
}
|
||||
|
||||
TypeEntry dummy;
|
||||
dummy.entryKind = TypeEntry::Primitive;
|
||||
dummy.primitiveKind = NodeKind::Hex8;
|
||||
dummy.displayName = "warmup";
|
||||
dummy.displayName = QStringLiteral("warmup");
|
||||
setTypes({dummy});
|
||||
popup(QPoint(-9999, -9999));
|
||||
hide();
|
||||
@@ -467,7 +492,7 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QString text = t.classKeyword.isEmpty()
|
||||
? t.displayName
|
||||
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16;
|
||||
int w = 10 + 20 + fm.horizontalAdvance(text) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(280, maxTextW + 24, 500);
|
||||
|
||||
@@ -1,62 +1,76 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Recursively add children of parentId as tree items under parentItem.
|
||||
inline void addWorkspaceChildren(QStandardItem* parentItem,
|
||||
const NodeTree& tree,
|
||||
uint64_t parentId,
|
||||
void* subPtr) {
|
||||
QVector<int> children = tree.childrenOf(parentId);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
struct TabInfo {
|
||||
const NodeTree* tree;
|
||||
QString name;
|
||||
void* subPtr; // QMdiSubWindow* as void*
|
||||
};
|
||||
|
||||
for (int idx : children) {
|
||||
const Node& node = tree.nodes[idx];
|
||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||
|
||||
// Skip hex preview nodes — they are padding/filler, not meaningful fields
|
||||
if (isHexNode(node.kind)) continue;
|
||||
|
||||
QString display;
|
||||
if (node.kind == NodeKind::Struct) {
|
||||
QString typeName = node.structTypeName.isEmpty()
|
||||
? node.name : node.structTypeName;
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(typeName, node.resolvedClassKeyword());
|
||||
} else {
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(node.name, QString::fromLatin1(kindToString(node.kind)));
|
||||
}
|
||||
|
||||
auto* item = new QStandardItem(display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
if (node.kind == NodeKind::Struct)
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll
|
||||
|
||||
if (node.kind == NodeKind::Struct)
|
||||
addWorkspaceChildren(item, tree, node.id, subPtr);
|
||||
|
||||
parentItem->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
inline void buildWorkspaceModel(QStandardItemModel* model,
|
||||
const NodeTree& tree,
|
||||
const QString& projectName,
|
||||
void* subPtr = nullptr) {
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
model->clear();
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
|
||||
auto* projectItem = new QStandardItem(projectName);
|
||||
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
// 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);
|
||||
|
||||
addWorkspaceChildren(projectItem, tree, 0, subPtr);
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
QVector<std::pair<const Node*, void*>> types, enums;
|
||||
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;
|
||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
enums.append({&n, tab.subPtr});
|
||||
else
|
||||
types.append({&n, tab.subPtr});
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
||||
const std::pair<const Node*, void*>& b) {
|
||||
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
for (const auto& [n, subPtr] : types) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
for (const auto& [n, subPtr] : enums) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
model->appendRow(projectItem);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ private slots:
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
|
||||
}
|
||||
|
||||
void testPaddingMarker() {
|
||||
void testHexNodeCompose() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -100,19 +100,18 @@ private slots:
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Padding;
|
||||
pad.name = "pad";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
tree.addNode(pad);
|
||||
Node hex;
|
||||
hex.kind = NodeKind::Hex8;
|
||||
hex.name = "pad";
|
||||
hex.parentId = rootId;
|
||||
hex.offset = 0;
|
||||
tree.addNode(hex);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + padding + root footer = 3
|
||||
// CommandRow + hex node + root footer = 3
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
|
||||
QCOMPARE(result.meta[1].depth, 1);
|
||||
|
||||
// Line 2 is root footer
|
||||
|
||||
@@ -34,9 +34,8 @@ static void buildSmallTree(NodeTree& tree) {
|
||||
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
||||
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
||||
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
||||
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding
|
||||
// Set padding arrayLen = 3 for 3-byte padding
|
||||
tree.nodes.last().arrayLen = 3;
|
||||
field(9, NodeKind::Hex16, "pad0"); // 2 bytes
|
||||
field(11, NodeKind::Hex8, "pad1"); // 1 byte
|
||||
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
||||
}
|
||||
|
||||
@@ -282,47 +281,6 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
}
|
||||
|
||||
// ── Test: Padding value edit is effectively blocked at controller level ──
|
||||
void testPaddingValueEditIsBlocked() {
|
||||
// Find the padding node
|
||||
int padIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; }
|
||||
}
|
||||
QVERIFY(padIdx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(padIdx);
|
||||
|
||||
// Read original data at padding offset
|
||||
int padSize = m_doc->tree.nodes[padIdx].byteSize();
|
||||
QByteArray origData = m_doc->provider->readBytes(addr, padSize);
|
||||
|
||||
// The context menu blocks Padding editing, so the controller's setNodeValue
|
||||
// would only be called if the editing UI somehow allows it. But let's verify
|
||||
// the editor correctly blocks it.
|
||||
// Find padding line in composed output
|
||||
ComposeResult result = m_doc->compose();
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// beginInlineEdit(Value) on Padding line must be rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Data must be unchanged
|
||||
QByteArray afterData = m_doc->provider->readBytes(addr, padSize);
|
||||
QCOMPARE(afterData, origData);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
||||
void testSetNodeValueHex() {
|
||||
int idx = -1;
|
||||
|
||||
@@ -170,9 +170,10 @@ static NodeTree makeTestTree() {
|
||||
n.parentId = rootId; n.offset = off;
|
||||
tree.addNode(n);
|
||||
};
|
||||
auto pad = [&](int off, int len, const char* name) {
|
||||
Node n; n.kind = NodeKind::Padding; n.name = name;
|
||||
n.parentId = rootId; n.offset = off; n.arrayLen = len;
|
||||
auto pad = [&](int off, int /*len*/, const char* name) {
|
||||
// 4-byte padding → Hex32 (all usages in this test pass len=4)
|
||||
Node n; n.kind = NodeKind::Hex32; n.name = name;
|
||||
n.parentId = rootId; n.offset = off;
|
||||
tree.addNode(n);
|
||||
};
|
||||
auto arr = [&](int off, NodeKind ek, int len, const char* name) {
|
||||
@@ -278,8 +279,8 @@ static NodeTree makeTestTree() {
|
||||
|
||||
n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n);
|
||||
n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n);
|
||||
n.kind = NodeKind::Padding; n.name = "Pad";
|
||||
n.offset = 4; n.arrayLen = 4; tree.addNode(n);
|
||||
n.kind = NodeKind::Hex32; n.name = "Pad";
|
||||
n.offset = 4; n.arrayLen = 1; tree.addNode(n);
|
||||
n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1;
|
||||
tree.addNode(n);
|
||||
}
|
||||
@@ -751,70 +752,6 @@ private slots:
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: Padding line rejects value editing ──
|
||||
void testPaddingLineRejectsValueEdit() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Find a Padding line in the composed output
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
m_result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(paddingLine >= 0, "Should have at least one Padding line in test tree");
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->nodeKind, NodeKind::Padding);
|
||||
|
||||
// Value edit on Padding MUST be rejected (the bug fix)
|
||||
QVERIFY2(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine),
|
||||
"Value edit should be rejected on Padding lines");
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Name edit on Padding SHOULD succeed (ASCII preview column is editable)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Name, paddingLine);
|
||||
QVERIFY2(ok, "Name edit should be allowed on Padding lines (ASCII preview)");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
|
||||
// Type edit on Padding SHOULD succeed (emits popup signal)
|
||||
QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested);
|
||||
ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine);
|
||||
QVERIFY2(ok, "Type edit should be allowed on Padding lines");
|
||||
QCOMPARE(typeSpy.count(), 1);
|
||||
}
|
||||
|
||||
// ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ──
|
||||
void testPaddingLineRejectsValueSpan() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Find a Padding line
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
m_result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
||||
QVERIFY(lm);
|
||||
|
||||
// valueSpanFor returns valid (shared with Hex via KF_HexPreview)
|
||||
ColumnSpan vs = RcxEditor::valueSpan(*lm, 200);
|
||||
QVERIFY2(vs.valid, "valueSpanFor should return valid for Padding (shared HexPreview flag)");
|
||||
|
||||
// But beginInlineEdit should still reject it
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Test: value edit commit fires signal with typed text ──
|
||||
void testValueEditCommitUpdatesSignal() {
|
||||
m_editor->applyDocument(m_result);
|
||||
@@ -823,8 +760,6 @@ private slots:
|
||||
const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||
QVERIFY(lm->nodeKind != NodeKind::Padding);
|
||||
|
||||
// Begin value edit
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
|
||||
QVERIFY(ok);
|
||||
|
||||
@@ -418,30 +418,6 @@ private slots:
|
||||
QVERIFY(result.contains("wchar_t wname[32];"));
|
||||
}
|
||||
|
||||
// ── Padding node ──
|
||||
|
||||
void testPaddingNode() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node pad;
|
||||
pad.kind = rcx::NodeKind::Padding;
|
||||
pad.name = "reserved";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
pad.arrayLen = 16;
|
||||
tree.addNode(pad);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
QVERIFY(result.contains("uint8_t reserved[16];"));
|
||||
}
|
||||
|
||||
// ── Full SDK export (multiple root structs) ──
|
||||
|
||||
void testFullSdkExport() {
|
||||
|
||||
@@ -304,39 +304,6 @@ private slots:
|
||||
QVERIFY(result.contains("float speed;"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_padding() {
|
||||
// Padding gap and tail padding should use aliased uint8_t
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node f2;
|
||||
f2.kind = NodeKind::UInt32;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8; // gap of 4 bytes at offset 4
|
||||
tree.addNode(f2);
|
||||
|
||||
QHash<NodeKind, QString> aliases;
|
||||
aliases[NodeKind::Padding] = "BYTE";
|
||||
|
||||
QString result = renderCpp(tree, rootId, &aliases);
|
||||
// Padding gap should use the alias
|
||||
QVERIFY(result.contains("BYTE _pad"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_array() {
|
||||
// Array element type should use alias
|
||||
NodeTree tree;
|
||||
@@ -547,134 +514,92 @@ private slots:
|
||||
void testWorkspace_simpleTree() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TestProject.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// 1 top-level item (the project)
|
||||
// Single "Project" root
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->text(), QString("TestProject.rcx"));
|
||||
QCOMPARE(project->text(), QString("Project"));
|
||||
|
||||
// Project has 1 child: the Player struct
|
||||
// 1 type directly under Project: Player (no member fields)
|
||||
QCOMPARE(project->rowCount(), 1);
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->text().contains("Player"));
|
||||
QVERIFY(player->text().contains("struct"));
|
||||
|
||||
// Player struct has 2 children: health, speed
|
||||
QCOMPARE(player->rowCount(), 2);
|
||||
QVERIFY(player->child(0)->text().contains("health"));
|
||||
QVERIFY(player->child(1)->text().contains("speed"));
|
||||
QVERIFY(project->child(0)->text().contains("Player"));
|
||||
QVERIFY(project->child(0)->text().contains("struct"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_twoRootTree() {
|
||||
auto tree = makeTwoRootTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TwoRoot.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
|
||||
// 2 root struct children: Alpha and Bravo
|
||||
// 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"));
|
||||
|
||||
// Each has 1 field child
|
||||
QCOMPARE(project->child(0)->rowCount(), 1);
|
||||
QVERIFY(project->child(0)->child(0)->text().contains("flagsA"));
|
||||
QCOMPARE(project->child(1)->rowCount(), 1);
|
||||
QVERIFY(project->child(1)->child(0)->text().contains("flagsB"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_rootCount() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->rowCount(), 3); // Pet, Cat, Ball
|
||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_petChildren() {
|
||||
void testWorkspace_richTree_sorted() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* pet = model.item(0)->child(0);
|
||||
QVERIFY(pet->text().contains("Pet"));
|
||||
// Pet has 2 non-hex children: name (UTF8), owner (Pointer64)
|
||||
QCOMPARE(pet->rowCount(), 2);
|
||||
QVERIFY(pet->child(0)->text().contains("name"));
|
||||
QVERIFY(pet->child(1)->text().contains("owner"));
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_catNesting() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* cat = model.item(0)->child(1);
|
||||
QVERIFY(cat->text().contains("Cat"));
|
||||
|
||||
// Find the nested "Pet" struct child (base)
|
||||
QStandardItem* base = nullptr;
|
||||
for (int i = 0; i < cat->rowCount(); i++) {
|
||||
if (cat->child(i)->text().contains("Pet") &&
|
||||
cat->child(i)->text().contains("struct")) {
|
||||
base = cat->child(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(base != nullptr, "Cat should have a nested Pet struct child");
|
||||
|
||||
// base has structId set
|
||||
QVERIFY(base->data(Qt::UserRole + 1).isValid());
|
||||
|
||||
// base should have its own children (name + owner)
|
||||
QCOMPARE(base->rowCount(), 2);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_ballChildren() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* ball = model.item(0)->child(2);
|
||||
QVERIFY(ball->text().contains("Ball"));
|
||||
|
||||
// Ball has 3 non-hex children: speed, position, color
|
||||
QCOMPARE(ball->rowCount(), 3);
|
||||
QVERIFY(ball->child(0)->text().contains("speed"));
|
||||
QVERIFY(ball->child(1)->text().contains("position"));
|
||||
QVERIFY(ball->child(2)->text().contains("color"));
|
||||
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);
|
||||
}
|
||||
|
||||
void testWorkspace_emptyTree() {
|
||||
NodeTree tree;
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Empty.rcx");
|
||||
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);
|
||||
}
|
||||
|
||||
void testWorkspace_structIdRole() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Test.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Project item should NOT have structId
|
||||
QVERIFY(!project->data(Qt::UserRole + 1).isValid());
|
||||
// Project root has kGroupSentinel
|
||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||
|
||||
// Player struct should have structId
|
||||
// Player type item should have structId
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||
|
||||
// health field should NOT have structId
|
||||
QStandardItem* health = player->child(0);
|
||||
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QStringListModel>
|
||||
#include <QLabel>
|
||||
#include <QFrame>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "typeselectorpopup.h"
|
||||
@@ -198,6 +200,127 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Isolate first-show cost with different window flags ──
|
||||
|
||||
void benchmarkFirstShow() {
|
||||
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
|
||||
|
||||
struct FlagTest {
|
||||
const char* name;
|
||||
Qt::WindowFlags flags;
|
||||
};
|
||||
FlagTest tests[] = {
|
||||
{"Qt::Popup|Frameless", Qt::Popup | Qt::FramelessWindowHint},
|
||||
{"Qt::Tool|Frameless", Qt::Tool | Qt::FramelessWindowHint},
|
||||
{"Qt::ToolTip", Qt::ToolTip},
|
||||
{"Qt::Window|Frameless", Qt::Window | Qt::FramelessWindowHint},
|
||||
{"Qt::Popup|Frameless (2nd)", Qt::Popup | Qt::FramelessWindowHint},
|
||||
};
|
||||
|
||||
for (const auto& test : tests) {
|
||||
auto* f = new QFrame(nullptr, test.flags);
|
||||
f->resize(300, 400);
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
f->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
f->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
t.restart();
|
||||
f->show();
|
||||
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t4 = t.nsecsElapsed();
|
||||
f->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug().noquote() << QString("=== %1 ===").arg(test.name);
|
||||
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||
delete f;
|
||||
}
|
||||
|
||||
// TypeSelectorPopup: cold vs after warmUp
|
||||
{
|
||||
auto* popup = new TypeSelectorPopup();
|
||||
TypeEntry dummy;
|
||||
dummy.entryKind = TypeEntry::Primitive;
|
||||
dummy.primitiveKind = NodeKind::Hex8;
|
||||
dummy.displayName = "test";
|
||||
popup->setTypes({dummy});
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
popup->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
t.restart();
|
||||
popup->show();
|
||||
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t4 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug().noquote() << QString("=== TypeSelectorPopup (cold, Qt::Popup) ===");
|
||||
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||
delete popup;
|
||||
}
|
||||
|
||||
// Clean order test: dummy popup with children FIRST, then TypeSelectorPopup
|
||||
qDebug() << "";
|
||||
qDebug() << "=== CLEAN: dummy popup first, then TypeSelectorPopup ===";
|
||||
{
|
||||
auto* dummy = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||
dummy->resize(300, 400);
|
||||
auto* dLay = new QVBoxLayout(dummy);
|
||||
dLay->addWidget(new QLabel("dummy"));
|
||||
dLay->addWidget(new QLineEdit);
|
||||
auto* dModel = new QStringListModel(dummy);
|
||||
QStringList dItems; for (int i = 0; i < 10; i++) dItems << "x";
|
||||
dModel->setStringList(dItems);
|
||||
auto* dLv = new QListView; dLv->setModel(dModel);
|
||||
dLay->addWidget(dLv);
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
dummy->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
dummy->hide();
|
||||
QApplication::processEvents();
|
||||
qDebug().noquote() << QString(" Dummy popup: show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||
delete dummy;
|
||||
}
|
||||
{
|
||||
auto* popup = new TypeSelectorPopup();
|
||||
TypeEntry e;
|
||||
e.entryKind = TypeEntry::Primitive;
|
||||
e.primitiveKind = NodeKind::Hex8;
|
||||
e.displayName = "test";
|
||||
popup->setTypes({e});
|
||||
popup->resize(300, 400);
|
||||
QElapsedTimer t; t.start();
|
||||
popup->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
qDebug().noquote() << QString(" TypeSelectorPopup (after dummy): show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||
delete popup;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Popup data model ──
|
||||
|
||||
void testPopupListsRootStructs() {
|
||||
|
||||
@@ -57,8 +57,8 @@ static void buildValidationTree(NodeTree& tree) {
|
||||
field(46, NodeKind::Hex32, "field_h32");
|
||||
field(50, NodeKind::Hex64, "field_h64");
|
||||
field(58, NodeKind::Pointer64, "field_ptr");
|
||||
field(66, NodeKind::Padding, "pad0");
|
||||
tree.nodes.last().arrayLen = 6;
|
||||
field(66, NodeKind::Hex32, "pad0");
|
||||
field(70, NodeKind::Hex16, "pad1");
|
||||
fieldArr(72, NodeKind::UInt32, 4, "field_arr");
|
||||
}
|
||||
|
||||
@@ -725,9 +725,9 @@ private slots:
|
||||
QCOMPARE(m_doc->undoStack.count(), 0);
|
||||
}
|
||||
|
||||
// ── changeNodeKind size transitions: shrink inserts padding ──
|
||||
// ── changeNodeKind size transitions: shrink inserts hex nodes ──
|
||||
|
||||
void testChangeKindShrinkInsertsPadding() {
|
||||
void testChangeKindShrinkInsertsHexNodes() {
|
||||
int idx = findNode(m_doc->tree, "field_u32");
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes
|
||||
@@ -737,7 +737,7 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8);
|
||||
// Should have inserted padding nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
||||
// Should have inserted hex nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
||||
QVERIFY(m_doc->tree.nodes.size() > origCount);
|
||||
|
||||
// Undo restores everything
|
||||
@@ -985,37 +985,6 @@ private slots:
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Editor: padding value edit blocked, name/type still work ──
|
||||
|
||||
void testPaddingEditRestrictions() {
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
ComposeResult result = m_doc->compose();
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find padding line
|
||||
int padLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
padLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(padLine >= 0);
|
||||
|
||||
// Value edit rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, padLine));
|
||||
|
||||
// Type edit accepted
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, padLine);
|
||||
QVERIFY(ok);
|
||||
m_editor->cancelInlineEdit();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
// ── Editor: struct header rejects value edit ──
|
||||
|
||||
void testStructHeaderRejectsValueEdit() {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
Reference in New Issue
Block a user