mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
14 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86499e58ee | ||
|
|
b2ae8d5a5d | ||
|
|
6768f04e9a | ||
|
|
c6e5f6508f | ||
|
|
e6529052b3 | ||
|
|
d43e989992 | ||
|
|
879e9f4047 | ||
|
|
e0d5a799b4 | ||
|
|
efae193520 | ||
|
|
ba1c2f8e5a | ||
|
|
5a0a4d1802 | ||
|
|
030eb34510 | ||
|
|
2939b25895 | ||
|
|
d38cb02fa2 |
72
.github/workflows/build.yml
vendored
72
.github/workflows/build.yml
vendored
@@ -2,7 +2,8 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -21,9 +22,9 @@ jobs:
|
||||
- name: Install Qt6 and MinGW
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_mingw'
|
||||
tools: 'tools_mingw1310,qt.tools.win64_mingw1310'
|
||||
version: "6.8.1"
|
||||
arch: "win64_mingw"
|
||||
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||
cache: true
|
||||
|
||||
- name: Configure
|
||||
@@ -83,7 +84,7 @@ jobs:
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
version: "6.8.1"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -140,9 +141,66 @@ jobs:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
qt_arch: clang_arm64
|
||||
artifact_name: Reclass-macos-arm64-qt6
|
||||
zip_name: Reclass-macos-arm64-qt6.zip
|
||||
- os: macos-15-intel
|
||||
qt_arch: clang_64
|
||||
artifact_name: Reclass-macos-x86_64-qt6
|
||||
zip_name: Reclass-macos-x86_64-qt6.zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake ninja qt
|
||||
|
||||
- name: Configure Qt paths
|
||||
run: |
|
||||
QT_PREFIX="$(brew --prefix qt)"
|
||||
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
|
||||
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package app zip
|
||||
run: |
|
||||
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
|
||||
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
|
||||
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
|
||||
fi
|
||||
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
|
||||
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
|
||||
codesign --force --deep --sign - build/Reclass.app
|
||||
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.zip_name }}
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux]
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -167,5 +225,7 @@ jobs:
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
|
||||
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
*.DS_Store
|
||||
|
||||
235
CMakeLists.txt
235
CMakeLists.txt
@@ -36,10 +36,35 @@ file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
|
||||
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
|
||||
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
|
||||
target_compile_features(raw_pdb PRIVATE cxx_std_11)
|
||||
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
|
||||
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
|
||||
# that pulls in the real CRT headers and strips __cdecl.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(raw_pdb PUBLIC
|
||||
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
|
||||
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
|
||||
message(STATUS "Generating fadec decode tables...")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
|
||||
"${FADEC_DIR}/instrs.txt"
|
||||
"${FADEC_DIR}/fadec-decode-public.inc"
|
||||
"${FADEC_DIR}/fadec-decode-private.inc"
|
||||
--32 --64
|
||||
RESULT_VARIABLE _fadec_result
|
||||
)
|
||||
if(NOT _fadec_result EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate fadec decode tables")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
@@ -88,6 +113,8 @@ add_executable(Reclass
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/macos_titlebar.h
|
||||
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
@@ -99,6 +126,16 @@ add_executable(Reclass
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(Reclass PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_ICON_FILE "class.icns"
|
||||
)
|
||||
target_sources(Reclass PRIVATE src/icons/class.icns)
|
||||
set_source_files_properties(src/icons/class.icns
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
endif()
|
||||
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
@@ -125,6 +162,12 @@ foreach(_tf ${_theme_files})
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_theme_files})
|
||||
set_source_files_properties(${_theme_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
@@ -133,6 +176,12 @@ foreach(_ef ${_example_files})
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_example_files})
|
||||
set_source_files_properties(${_example_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||
endif()
|
||||
|
||||
include(deploy)
|
||||
|
||||
|
||||
@@ -273,178 +322,178 @@ if(BUILD_TESTING)
|
||||
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
||||
if(BUILD_UI_TESTS)
|
||||
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_validation PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
target_include_directories(test_validation PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_visibility PRIVATE
|
||||
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_visibility PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||
src/resources.qrc)
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
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)
|
||||
endif()
|
||||
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||
target_link_libraries(bench_large_class PRIVATE
|
||||
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||
target_link_libraries(bench_large_class PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
COMMAND $<TARGET_FILE:${QT}::windeployqt>
|
||||
--no-compiler-runtime --no-translations
|
||||
--no-opengl-sw --no-system-d3d-compiler
|
||||
@@ -452,12 +501,14 @@ if(BUILD_TESTING)
|
||||
DEPENDS test_controller
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
|
||||
14
README.md
14
README.md
@@ -49,7 +49,7 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
|
||||
- [ ] Safe mode
|
||||
- [ ] File import for other Reclass instances
|
||||
- [ ] Expose UI functionality to plugins
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] iOS support
|
||||
- [ ] Display RTTI information
|
||||
|
||||
## Data Sources
|
||||
@@ -92,7 +92,7 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `
|
||||
|
||||
### Quick Build
|
||||
|
||||
```bash
|
||||
```/dev/null/commands.sh#L1-4
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
@@ -101,6 +101,16 @@ cd Reclass
|
||||
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
### macOS Build
|
||||
|
||||
```/dev/null/commands.sh#L1-2
|
||||
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
|
||||
```
|
||||
|
||||
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
|
||||
|
||||
Note: macOS Gatekeeper may block unsigned apps. If the app won’t open, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
|
||||
### Manual Build (MinGW)
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
|
||||
29
cmake/raw_pdb_prefix.h
Normal file
29
cmake/raw_pdb_prefix.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Force-included before every raw_pdb translation unit (and consumers).
|
||||
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
|
||||
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
|
||||
//
|
||||
// Fix: include the real CRT headers, then include PDB_CRT.h with function
|
||||
// names macro-renamed to harmless dummies. This triggers #pragma once so
|
||||
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
|
||||
//
|
||||
// Guarded with __cplusplus because PUBLIC propagation applies this to C
|
||||
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
|
||||
#ifdef __cplusplus
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#undef __cdecl
|
||||
#define __cdecl
|
||||
|
||||
#define printf _pdb_crt_unused_printf
|
||||
#define memcmp _pdb_crt_unused_memcmp
|
||||
#define memcpy _pdb_crt_unused_memcpy
|
||||
#define strlen _pdb_crt_unused_strlen
|
||||
#define strcmp _pdb_crt_unused_strcmp
|
||||
#include "Foundation/PDB_CRT.h"
|
||||
#undef printf
|
||||
#undef memcmp
|
||||
#undef memcpy
|
||||
#undef strlen
|
||||
#undef strcmp
|
||||
#endif
|
||||
168
scripts/build_macos.sh
Executable file
168
scripts/build_macos.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
print_help() {
|
||||
cat <<'EOF'
|
||||
Reclass macOS Build Script
|
||||
|
||||
Usage:
|
||||
./scripts/build_macos.sh [options]
|
||||
|
||||
Options:
|
||||
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
|
||||
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
|
||||
--build-dir <path> Build directory (default: <repo>/build)
|
||||
--generator <name> CMake generator (default: Ninja if available)
|
||||
--clean Remove build directory before configuring
|
||||
--rebuild Clean then build
|
||||
--package Run macdeployqt and create a zip
|
||||
--tests Run ctest after build
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
|
||||
- If Qt is installed via Homebrew, the script will try to detect it.
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/.." && pwd)"
|
||||
|
||||
qt_dir=""
|
||||
build_type="Release"
|
||||
build_dir="${project_root}/build"
|
||||
generator=""
|
||||
do_clean="false"
|
||||
do_package="false"
|
||||
do_tests="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--qt-dir)
|
||||
qt_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-type)
|
||||
build_type="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
build_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--generator)
|
||||
generator="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--rebuild)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--package)
|
||||
do_package="true"
|
||||
shift
|
||||
;;
|
||||
--tests)
|
||||
do_tests="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
print_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${qt_dir}" ]]; then
|
||||
if [[ -n "${QTDIR:-}" ]]; then
|
||||
qt_dir="${QTDIR}"
|
||||
elif [[ -n "${Qt6_DIR:-}" ]]; then
|
||||
qt_dir="${Qt6_DIR}"
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
if brew --prefix qt >/dev/null 2>&1; then
|
||||
qt_dir="$(brew --prefix qt)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "ERROR: cmake not found. Install CMake and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${generator}" ]]; then
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
generator="Ninja"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
|
||||
echo "Cleaning build directory: ${build_dir}"
|
||||
rm -rf "${build_dir}"
|
||||
fi
|
||||
|
||||
mkdir -p "${build_dir}"
|
||||
|
||||
cmake_args=(
|
||||
-S "${project_root}"
|
||||
-B "${build_dir}"
|
||||
-DCMAKE_BUILD_TYPE="${build_type}"
|
||||
)
|
||||
|
||||
if [[ -n "${generator}" ]]; then
|
||||
cmake_args+=(-G "${generator}")
|
||||
fi
|
||||
|
||||
if [[ -n "${qt_dir}" ]]; then
|
||||
export PATH="${qt_dir}/bin:${PATH}"
|
||||
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
|
||||
fi
|
||||
|
||||
echo "Configuring..."
|
||||
cmake "${cmake_args[@]}"
|
||||
|
||||
echo "Building..."
|
||||
cmake --build "${build_dir}" --config "${build_type}"
|
||||
|
||||
if [[ "${do_tests}" == "true" ]]; then
|
||||
echo "Running tests..."
|
||||
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
|
||||
fi
|
||||
|
||||
if [[ "${do_package}" == "true" ]]; then
|
||||
app_path="${build_dir}/Reclass.app"
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
echo "ERROR: ${app_path} not found. Build may have failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
macdeployqt_bin=""
|
||||
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
|
||||
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
|
||||
elif command -v macdeployqt >/dev/null 2>&1; then
|
||||
macdeployqt_bin="$(command -v macdeployqt)"
|
||||
fi
|
||||
|
||||
if [[ -z "${macdeployqt_bin}" ]]; then
|
||||
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running macdeployqt..."
|
||||
"${macdeployqt_bin}" "${app_path}" -always-overwrite
|
||||
|
||||
arch="$(uname -m)"
|
||||
zip_name="Reclass-macos-${arch}-qt6.zip"
|
||||
echo "Creating zip: ${zip_name}"
|
||||
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
|
||||
echo "Packaged: ${build_dir}/${zip_name}"
|
||||
fi
|
||||
@@ -244,6 +244,17 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
showTypePopup(editor, mode, nodeIdx, globalPos);
|
||||
});
|
||||
|
||||
// Insert key shortcut
|
||||
connect(editor, &RcxEditor::insertAboveRequested,
|
||||
this, [this](int nodeIdx, NodeKind kind) {
|
||||
if (nodeIdx >= 0)
|
||||
insertNodeAbove(nodeIdx, kind, QStringLiteral("field"));
|
||||
else {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, kind, QStringLiteral("field"));
|
||||
}
|
||||
});
|
||||
|
||||
// Inline editing signals
|
||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
|
||||
@@ -708,6 +719,15 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
|
||||
|
||||
// Hex nodes don't display names (ASCII preview instead), so the stored
|
||||
// name may be empty or stale. Give it a sensible default.
|
||||
if (isHexNode(node.kind) && !isHexNode(newKind)) {
|
||||
QString autoName = QStringLiteral("field_%1")
|
||||
.arg(node.offset, 4, 16, QChar('0'));
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Rename{node.id, node.name, autoName}));
|
||||
}
|
||||
|
||||
// Insert hex nodes to fill the gap (largest first for alignment)
|
||||
int padOffset = baseOffset;
|
||||
while (gap > 0) {
|
||||
@@ -741,8 +761,19 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
||||
adjs.append({sib.id, sib.offset, sib.offset + delta});
|
||||
}
|
||||
}
|
||||
bool needsRename = isHexNode(node.kind) && !isHexNode(newKind);
|
||||
if (needsRename) {
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Change type"));
|
||||
}
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::ChangeKind{node.id, node.kind, newKind, adjs}));
|
||||
if (needsRename) {
|
||||
QString autoName = QStringLiteral("field_%1")
|
||||
.arg(node.offset, 4, 16, QChar('0'));
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Rename{node.id, node.name, autoName}));
|
||||
m_doc->undoStack.endMacro();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,6 +813,31 @@ void RcxController::insertNode(uint64_t parentId, int offset, NodeKind kind, con
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n}));
|
||||
}
|
||||
|
||||
void RcxController::insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name) {
|
||||
if (beforeIdx < 0 || beforeIdx >= m_doc->tree.nodes.size()) return;
|
||||
const Node& before = m_doc->tree.nodes[beforeIdx];
|
||||
|
||||
Node n;
|
||||
n.kind = kind;
|
||||
n.name = name;
|
||||
n.parentId = before.parentId;
|
||||
n.offset = before.offset;
|
||||
n.id = m_doc->tree.reserveId();
|
||||
|
||||
int insertSize = sizeForKind(kind);
|
||||
|
||||
// Shift siblings at or after the insertion offset down
|
||||
QVector<cmd::OffsetAdj> adjs;
|
||||
auto siblings = m_doc->tree.childrenOf(before.parentId);
|
||||
for (int si : siblings) {
|
||||
auto& sib = m_doc->tree.nodes[si];
|
||||
if (sib.offset >= before.offset)
|
||||
adjs.append({sib.id, sib.offset, sib.offset + insertSize});
|
||||
}
|
||||
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
||||
}
|
||||
|
||||
void RcxController::removeNode(int nodeIdx) {
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
@@ -1558,6 +1614,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
return indices;
|
||||
};
|
||||
|
||||
// ── Insert shortcuts (always available) ──
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
||||
});
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
||||
});
|
||||
menu.addSeparator();
|
||||
|
||||
// Quick-convert shortcuts when all selected nodes share the same kind
|
||||
NodeKind commonKind = NodeKind::Hex64;
|
||||
bool allSame = true;
|
||||
@@ -1581,6 +1648,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
} else if (commonKind == NodeKind::Hex32) {
|
||||
menu.addAction("Change to uint32_t", [this, collectIndices]() {
|
||||
batchChangeKind(collectIndices(), NodeKind::UInt32); });
|
||||
menu.addAction("Change to float", [this, collectIndices]() {
|
||||
batchChangeKind(collectIndices(), NodeKind::Float); });
|
||||
addedQuickConvert = true;
|
||||
} else if (commonKind == NodeKind::Hex16) {
|
||||
menu.addAction("Change to int16_t", [this, collectIndices]() {
|
||||
@@ -1629,6 +1698,23 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
act->setChecked(m_trackValues);
|
||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||
}
|
||||
{
|
||||
auto* act = menu.addAction("Clear Value History");
|
||||
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
|
||||
connect(act, &QAction::triggered, this, [this, ids]() {
|
||||
for (uint64_t id : ids) {
|
||||
m_valueHistory.remove(id);
|
||||
for (int ci : m_doc->tree.subtreeIndices(id))
|
||||
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
||||
}
|
||||
m_refreshGen++; // discard in-flight async reads
|
||||
m_prevPages.clear(); // clean baseline for next read cycle
|
||||
m_changedOffsets.clear(); // no phantom change indicators
|
||||
refresh();
|
||||
for (auto* editor : m_editors)
|
||||
editor->dismissHistoryPopup();
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// Check if all selected nodes share the same parent (required for grouping)
|
||||
@@ -1658,7 +1744,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
||||
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
||||
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
|
||||
QStringList addrs;
|
||||
for (uint64_t id : ids) {
|
||||
int ni = m_doc->tree.indexOfId(id);
|
||||
@@ -1675,6 +1762,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
QMenu menu;
|
||||
|
||||
// ── Insert shortcuts (at very top) ──
|
||||
if (hasNode) {
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 4 Above\tShift+Ins",
|
||||
[this, nodeIdx]() {
|
||||
insertNodeAbove(nodeIdx, NodeKind::Hex32, QStringLiteral("field"));
|
||||
});
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 8 Above\tIns",
|
||||
[this, nodeIdx]() {
|
||||
insertNodeAbove(nodeIdx, NodeKind::Hex64, QStringLiteral("field"));
|
||||
});
|
||||
} else {
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 4", [this]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, NodeKind::Hex32, QStringLiteral("field"));
|
||||
});
|
||||
menu.addAction(icon("diff-added.svg"), "Insert 8", [this]() {
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
insertNode(target, -1, NodeKind::Hex64, QStringLiteral("field"));
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// ── Node-specific actions (only when clicking on a node) ──
|
||||
if (hasNode) {
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
@@ -1723,6 +1832,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||
});
|
||||
menu.addAction("Change to float", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::Float);
|
||||
});
|
||||
addedQuickConvert = true;
|
||||
} else if (node.kind == NodeKind::Hex16) {
|
||||
menu.addAction("Change to int16_t", [this, nodeId]() {
|
||||
@@ -1812,6 +1925,21 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
act->setChecked(m_trackValues);
|
||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||
}
|
||||
{
|
||||
auto* act = menu.addAction("Clear Value History");
|
||||
act->setToolTip(QStringLiteral("Reset change tracking for this node"));
|
||||
connect(act, &QAction::triggered, this, [this, nodeId]() {
|
||||
m_valueHistory.remove(nodeId);
|
||||
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
||||
m_valueHistory.remove(m_doc->tree.nodes[ci].id);
|
||||
m_refreshGen++; // discard in-flight async reads
|
||||
m_prevPages.clear(); // clean baseline for next read cycle
|
||||
m_changedOffsets.clear(); // no phantom change indicators
|
||||
refresh();
|
||||
for (auto* editor : m_editors)
|
||||
editor->dismissHistoryPopup();
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
|
||||
@@ -1961,24 +2089,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
if (ni >= 0) removeNode(ni);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(icon("link.svg"), "Copy &Address", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||
QApplication::clipboard()->setText(
|
||||
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
||||
});
|
||||
|
||||
menu.addAction(icon("whole-word.svg"), "Copy &Offset", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni < 0) return;
|
||||
int off = m_doc->tree.nodes[ni].offset;
|
||||
QApplication::clipboard()->setText(
|
||||
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
} // else (non-member node actions)
|
||||
}
|
||||
@@ -2059,10 +2169,46 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(icon("clippy.svg"), "Copy All as Text", [editor]() {
|
||||
QMenu* copyMenu = menu.addMenu(icon("clippy.svg"), "Copy");
|
||||
if (hasNode) {
|
||||
uint64_t copyNodeId = m_doc->tree.nodes[nodeIdx].id;
|
||||
copyMenu->addAction(icon("link.svg"), "Copy &Address", [this, copyNodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(copyNodeId);
|
||||
if (ni < 0) return;
|
||||
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
|
||||
QApplication::clipboard()->setText(
|
||||
QStringLiteral("0x") + QString::number(addr, 16).toUpper());
|
||||
});
|
||||
copyMenu->addAction(icon("whole-word.svg"), "Copy &Offset", [this, copyNodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(copyNodeId);
|
||||
if (ni < 0) return;
|
||||
int off = m_doc->tree.nodes[ni].offset;
|
||||
QApplication::clipboard()->setText(
|
||||
QStringLiteral("+0x") + QString::number(off, 16).toUpper().rightJustified(4, '0'));
|
||||
});
|
||||
copyMenu->addSeparator();
|
||||
}
|
||||
copyMenu->addAction("Copy Line", [editor, line]() {
|
||||
auto* sci = editor->scintilla();
|
||||
int len = (int)sci->SendScintilla(QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
||||
if (len > 0) {
|
||||
QByteArray buf(len + 1, '\0');
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)line, (void*)buf.data());
|
||||
QString text = QString::fromUtf8(buf.data(), len).trimmed();
|
||||
if (!text.isEmpty())
|
||||
QApplication::clipboard()->setText(text);
|
||||
}
|
||||
});
|
||||
copyMenu->addAction("Copy All as Text", [editor]() {
|
||||
QApplication::clipboard()->setText(editor->textWithMargins());
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(icon("search.svg"), "Search...\tCtrl+F", [editor]() {
|
||||
QTimer::singleShot(0, editor, &RcxEditor::showFindBar);
|
||||
});
|
||||
|
||||
menu.exec(globalPos);
|
||||
}
|
||||
|
||||
@@ -2934,7 +3080,32 @@ void RcxController::selectSource(const QString& text) {
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
||||
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
|
||||
|
||||
// Re-evaluate formula if present (mirrors attachViaPlugin)
|
||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||
AddressParserCallbacks cbs;
|
||||
auto* prov = m_doc->provider.get();
|
||||
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
||||
uint64_t base = prov->symbolToAddress(name);
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
int ptrSz = m_doc->tree.pointerSize;
|
||||
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = prov->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
auto result = AddressParser::evaluate(
|
||||
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
||||
if (result.ok)
|
||||
m_doc->tree.baseAddress = result.value;
|
||||
} else if (newBase != 0 && m_doc->tree.baseAddress == 0x00400000) {
|
||||
// Only apply provider base for fresh/default projects.
|
||||
// If user loaded an .rcx with a custom base, preserve it.
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
}
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ public:
|
||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||
void renameNode(int nodeIdx, const QString& newName);
|
||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
||||
void removeNode(int nodeIdx);
|
||||
void toggleCollapse(int nodeIdx);
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
@@ -147,8 +148,9 @@ public:
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessor
|
||||
// Test accessors
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
|
||||
18
src/core.h
18
src/core.h
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
@@ -500,6 +501,7 @@ struct NodeTree {
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
@@ -509,10 +511,16 @@ struct ValueHistory {
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
count = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
@@ -536,6 +544,16 @@ struct ValueHistory {
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
|
||||
// Iterate with timestamps from newest to oldest
|
||||
template<typename Fn>
|
||||
void forEachWithTime(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = (head + kCapacity - 1 - i) % kCapacity;
|
||||
fn(values[idx], timestamps[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
|
||||
179
src/editor.cpp
179
src/editor.cpp
@@ -19,8 +19,10 @@
|
||||
#include <QClipboard>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QLineEdit>
|
||||
#include <QScreen>
|
||||
#include <QScrollBar>
|
||||
#include <QDateTime>
|
||||
#include <functional>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
@@ -102,7 +104,8 @@ public:
|
||||
sep->setPalette(sp);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
for (const QString& v : vals) {
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
hist.forEachWithTime([&](const QString& v, qint64 msec) {
|
||||
auto* row = new QHBoxLayout;
|
||||
row->setContentsMargins(0, 1, 0, 1);
|
||||
row->setSpacing(8);
|
||||
@@ -113,6 +116,24 @@ public:
|
||||
row->addWidget(label, 1);
|
||||
m_labels.append(label);
|
||||
|
||||
if (msec > 0) {
|
||||
qint64 elapsed = now - msec;
|
||||
QString timeStr;
|
||||
if (elapsed < 1000)
|
||||
timeStr = QStringLiteral("now");
|
||||
else if (elapsed < 60000)
|
||||
timeStr = QStringLiteral("%1s ago").arg(elapsed / 1000);
|
||||
else if (elapsed < 3600000)
|
||||
timeStr = QStringLiteral("%1m ago").arg(elapsed / 60000);
|
||||
else
|
||||
timeStr = QStringLiteral("%1h ago").arg(elapsed / 3600000);
|
||||
|
||||
auto* timeLabel = new QLabel(timeStr);
|
||||
timeLabel->setFont(font);
|
||||
timeLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
row->addWidget(timeLabel);
|
||||
}
|
||||
|
||||
if (showButtons) {
|
||||
auto* setBtn = new QToolButton;
|
||||
setBtn->setText(QStringLiteral("Set"));
|
||||
@@ -130,7 +151,7 @@ public:
|
||||
row->addWidget(setBtn);
|
||||
}
|
||||
vbox->addLayout(row);
|
||||
}
|
||||
});
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
@@ -364,6 +385,7 @@ static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
||||
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
||||
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||
static constexpr int IND_FIND = 19; // Search match highlight
|
||||
|
||||
static QString g_fontName = "JetBrains Mono";
|
||||
|
||||
@@ -380,6 +402,29 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci = new QsciScintilla(this);
|
||||
layout->addWidget(m_sci);
|
||||
|
||||
// Find bar (hidden by default, shown with Ctrl+F)
|
||||
m_findBarContainer = new QWidget(this);
|
||||
auto* fbLayout = new QHBoxLayout(m_findBarContainer);
|
||||
fbLayout->setContentsMargins(4, 0, 0, 0);
|
||||
fbLayout->setSpacing(2);
|
||||
auto* findPrevBtn = new QToolButton(m_findBarContainer);
|
||||
findPrevBtn->setText(QStringLiteral("\u25C0"));
|
||||
findPrevBtn->setFixedSize(24, 24);
|
||||
auto* findNextBtn = new QToolButton(m_findBarContainer);
|
||||
findNextBtn->setText(QStringLiteral("\u25B6"));
|
||||
findNextBtn->setFixedSize(24, 24);
|
||||
auto* findCloseBtn = new QToolButton(m_findBarContainer);
|
||||
findCloseBtn->setText(QStringLiteral("\u2715"));
|
||||
findCloseBtn->setFixedSize(24, 24);
|
||||
m_findBar = new QLineEdit(m_findBarContainer);
|
||||
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
||||
fbLayout->addWidget(findPrevBtn);
|
||||
fbLayout->addWidget(findNextBtn);
|
||||
fbLayout->addWidget(findCloseBtn);
|
||||
fbLayout->addWidget(m_findBar);
|
||||
m_findBarContainer->setVisible(false);
|
||||
layout->addWidget(m_findBarContainer);
|
||||
|
||||
setupScintilla();
|
||||
setupLexer();
|
||||
setupMargins();
|
||||
@@ -395,6 +440,55 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci->viewport()->installEventFilter(this);
|
||||
m_sci->viewport()->setMouseTracking(true);
|
||||
|
||||
// Find bar: indicator-based search (selection is disabled in our Scintilla)
|
||||
auto doFind = [this](bool forward) {
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
||||
|
||||
QString text = m_findBar->text();
|
||||
if (text.isEmpty()) return;
|
||||
QByteArray needle = text.toUtf8();
|
||||
|
||||
long startPos = forward ? m_findPos : (m_findPos > 0 ? m_findPos - 1 : docLen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, startPos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
||||
forward ? docLen : (long)0);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
||||
|
||||
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)needle.size(), needle.constData());
|
||||
if (pos == -1) { // wrap
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART,
|
||||
forward ? (long)0 : docLen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND,
|
||||
forward ? startPos : (long)0);
|
||||
pos = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)needle.size(), needle.constData());
|
||||
}
|
||||
if (pos >= 0) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, (long)needle.size());
|
||||
int line = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_LINEFROMPOSITION, pos);
|
||||
m_sci->ensureLineVisible(line);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos);
|
||||
m_findPos = pos + (forward ? needle.size() : 0);
|
||||
}
|
||||
};
|
||||
connect(m_findBar, &QLineEdit::textChanged, this, [doFind]() { doFind(true); });
|
||||
connect(m_findBar, &QLineEdit::returnPressed, this, [doFind]() { doFind(true); });
|
||||
connect(findNextBtn, &QToolButton::clicked, this, [doFind]() { doFind(true); });
|
||||
connect(findPrevBtn, &QToolButton::clicked, this, [doFind]() { doFind(false); });
|
||||
connect(findCloseBtn, &QToolButton::clicked, this, [this]() { hideFindBar(); });
|
||||
// Escape hides find bar
|
||||
{
|
||||
auto* escAction = new QAction(m_findBar);
|
||||
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
||||
escAction->setShortcutContext(Qt::WidgetShortcut);
|
||||
m_findBar->addAction(escAction);
|
||||
connect(escAction, &QAction::triggered, this, [this]() { hideFindBar(); });
|
||||
}
|
||||
|
||||
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
|
||||
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
|
||||
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
@@ -598,6 +692,12 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
|
||||
|
||||
// Find match highlight — thick underline (avoids box rendering artifacts)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||
IND_FIND, (long)1);
|
||||
|
||||
}
|
||||
|
||||
void RcxEditor::setupLexer() {
|
||||
@@ -734,6 +834,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
IND_HINT_GREEN, theme.indHintGreen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_LOCAL_OFF, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_FIND, theme.borderFocused);
|
||||
|
||||
// Lexer colors
|
||||
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
||||
@@ -782,6 +884,20 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
abs, theme.background);
|
||||
}
|
||||
}
|
||||
|
||||
// Find bar
|
||||
if (m_findBarContainer) {
|
||||
m_findBar->setStyleSheet(
|
||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" padding: 4px 8px; font-size: 13px; }")
|
||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
||||
m_findBarContainer->setStyleSheet(
|
||||
QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
||||
"QToolButton:hover { background: %4; }"
|
||||
"QToolButton:pressed { background: %5; }")
|
||||
.arg(theme.background.name(), theme.text.name(), theme.border.name(),
|
||||
theme.hover.name(), theme.backgroundAlt.name()));
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
@@ -863,6 +979,27 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_prevHoveredNodeId = 0;
|
||||
m_prevHoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
|
||||
// Re-apply find indicator (setText() clears all indicators)
|
||||
if (m_findBarContainer && m_findBarContainer->isVisible()) {
|
||||
QString needle = m_findBar->text();
|
||||
if (!needle.isEmpty()) {
|
||||
QByteArray nb = needle.toUtf8();
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEARCHFLAGS, (long)0);
|
||||
long pos = 0;
|
||||
while (pos < docLen) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, pos);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, docLen);
|
||||
long found = m_sci->SendScintilla(QsciScintillaBase::SCI_SEARCHINTARGET,
|
||||
(uintptr_t)nb.size(), nb.constData());
|
||||
if (found < 0) break;
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, found, (long)nb.size());
|
||||
pos = found + nb.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||
@@ -1243,6 +1380,27 @@ int RcxEditor::currentNodeIndex() const {
|
||||
return lm ? lm->nodeIdx : -1;
|
||||
}
|
||||
|
||||
void RcxEditor::showFindBar() {
|
||||
m_findBarContainer->setVisible(true);
|
||||
m_findBar->setFocus();
|
||||
m_findBar->selectAll();
|
||||
m_findPos = 0;
|
||||
}
|
||||
|
||||
void RcxEditor::dismissHistoryPopup() {
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
void RcxEditor::hideFindBar() {
|
||||
m_findBarContainer->setVisible(false);
|
||||
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
||||
m_findPos = 0;
|
||||
m_sci->setFocus();
|
||||
}
|
||||
|
||||
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
|
||||
@@ -1318,7 +1476,12 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
|
||||
if (heat <= 0) continue;
|
||||
if (heat <= 0) {
|
||||
// Clear any stale heat indicators from a previous frame
|
||||
for (int hi : heatIndicators)
|
||||
clearIndicatorLine(hi, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||
@@ -1810,6 +1973,10 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (obj == m_sci && event->type() == QEvent::KeyPress) {
|
||||
auto* ke = static_cast<QKeyEvent*>(event);
|
||||
if (ke->matches(QKeySequence::Find)) {
|
||||
showFindBar();
|
||||
return true;
|
||||
}
|
||||
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
|
||||
if (!handled && !m_editState.active) {
|
||||
// Clear hover on keyboard navigation (stale after scroll)
|
||||
@@ -2085,6 +2252,12 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
return beginInlineEdit(EditTarget::Value);
|
||||
case Qt::Key_Insert:
|
||||
if (ke->modifiers() & Qt::ShiftModifier)
|
||||
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex32);
|
||||
else
|
||||
emit insertAboveRequested(currentNodeIndex(), NodeKind::Hex64);
|
||||
return true;
|
||||
case Qt::Key_Tab: {
|
||||
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
||||
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
||||
|
||||
10
src/editor.h
10
src/editor.h
@@ -6,6 +6,7 @@
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QLineEdit;
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
|
||||
@@ -32,6 +33,8 @@ public:
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -78,6 +81,7 @@ signals:
|
||||
void inlineEditCancelled();
|
||||
void typeSelectorRequested();
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
@@ -154,6 +158,12 @@ private:
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Find bar ──
|
||||
QWidget* m_findBarContainer = nullptr;
|
||||
QLineEdit* m_findBar = nullptr;
|
||||
long m_findPos = 0;
|
||||
void hideFindBar();
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
|
||||
11329
src/examples/t6zm.rcx
Normal file
11329
src/examples/t6zm.rcx
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/icons/class.icns
Normal file
BIN
src/icons/class.icns
Normal file
Binary file not shown.
13
src/macos_titlebar.h
Normal file
13
src/macos_titlebar.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme;
|
||||
|
||||
// Apply macOS native title bar color to match the theme.
|
||||
// No-op on non-macOS platforms (implementation is platform-specific).
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
|
||||
|
||||
} // namespace rcx
|
||||
43
src/macos_titlebar.mm
Normal file
43
src/macos_titlebar.mm
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "macos_titlebar.h"
|
||||
#include "themes/theme.h"
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <QColor>
|
||||
#include <QWidget>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
static NSColor* toNSColor(const QColor& color) {
|
||||
return [NSColor colorWithCalibratedRed:color.redF()
|
||||
green:color.greenF()
|
||||
blue:color.blueF()
|
||||
alpha:color.alphaF()];
|
||||
}
|
||||
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme) {
|
||||
if (!window) return;
|
||||
|
||||
// Ensure native window is created.
|
||||
window->winId();
|
||||
|
||||
auto* nsView = reinterpret_cast<NSView*>(window->winId());
|
||||
if (!nsView) return;
|
||||
|
||||
NSWindow* nsWindow = [nsView window];
|
||||
if (!nsWindow) return;
|
||||
|
||||
// Keep native traffic lights while tinting the title bar to the theme.
|
||||
// Match the title text contrast by selecting the appropriate system appearance.
|
||||
const qreal luminance =
|
||||
0.2126 * theme.background.redF() +
|
||||
0.7152 * theme.background.greenF() +
|
||||
0.0722 * theme.background.blueF();
|
||||
const bool isLight = luminance >= 0.5;
|
||||
[nsWindow setAppearance:[NSAppearance appearanceNamed:
|
||||
(isLight ? NSAppearanceNameAqua : NSAppearanceNameDarkAqua)]];
|
||||
[nsWindow setTitlebarAppearsTransparent:YES];
|
||||
[nsWindow setTitleVisibility:NSWindowTitleVisible];
|
||||
[nsWindow setBackgroundColor:toNSColor(theme.background)];
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
323
src/main.cpp
323
src/main.cpp
@@ -53,7 +53,6 @@
|
||||
#include "themes/thememanager.h"
|
||||
#include "themes/themeeditor.h"
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
@@ -351,7 +350,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.selection);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||
@@ -389,13 +388,18 @@ public:
|
||||
|
||||
namespace rcx {
|
||||
|
||||
#ifdef __APPLE__
|
||||
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
|
||||
#endif
|
||||
|
||||
// MainWindow class declaration is in mainwindow.h
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
setWindowTitle("Reclass");
|
||||
resize(1200, 800);
|
||||
|
||||
// Frameless window with system menu (Alt+Space) and min/max/close support
|
||||
#ifndef __APPLE__
|
||||
// Frameless window with system menu (Alt+Space) and min/max/close support.
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint
|
||||
| Qt::WindowMinMaxButtonsHint);
|
||||
|
||||
@@ -403,6 +407,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
m_titleBar = new TitleBarWidget(this);
|
||||
m_titleBar->applyTheme(ThemeManager::instance().current());
|
||||
setMenuWidget(m_titleBar);
|
||||
m_menuBar = m_titleBar->menuBar();
|
||||
#else
|
||||
setWindowTitle(QStringLiteral("Reclass"));
|
||||
setUnifiedTitleAndToolBarOnMac(true);
|
||||
m_menuBar = menuBar();
|
||||
m_menuBar->setNativeMenuBar(true);
|
||||
applyMacTitleBarTheme(this, ThemeManager::instance().current());
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// 1px top margin preserves DWM drop shadow on the frameless window
|
||||
@@ -454,8 +466,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
// Restore menu bar title case setting (after menus are created)
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", false).toBool());
|
||||
if (s.value("showIcon", false).toBool())
|
||||
m_menuBarTitleCase = s.value("menuBarTitleCase", false).toBool();
|
||||
applyMenuBarTitleCase(m_menuBarTitleCase);
|
||||
if (m_titleBar && s.value("showIcon", false).toBool())
|
||||
m_titleBar->setShowIcon(true);
|
||||
}
|
||||
|
||||
@@ -507,13 +520,48 @@ inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequ
|
||||
return result;
|
||||
}
|
||||
|
||||
void MainWindow::applyMenuBarTitleCase(bool titleCase) {
|
||||
m_menuBarTitleCase = titleCase;
|
||||
if (m_titleBar) {
|
||||
m_titleBar->setMenuBarTitleCase(titleCase);
|
||||
return;
|
||||
}
|
||||
if (!m_menuBar) return;
|
||||
|
||||
for (QAction* action : m_menuBar->actions()) {
|
||||
QString text = action->text();
|
||||
QString clean = text;
|
||||
clean.remove('&');
|
||||
|
||||
if (titleCase) {
|
||||
action->setText("&" + clean.toUpper());
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::createMenus() {
|
||||
// File
|
||||
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
||||
auto* file = m_menuBar->addMenu("&File");
|
||||
Qt5Qt6AddAction(file, "New &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass);
|
||||
Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct);
|
||||
Qt5Qt6AddAction(file, "New &Enum", QKeySequence(Qt::CTRL | Qt::Key_E), QIcon(), this, &MainWindow::newEnum);
|
||||
Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
|
||||
m_recentFilesMenu = file->addMenu("Recent &Files");
|
||||
updateRecentFilesMenu();
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||
@@ -527,7 +575,11 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
// Examples submenu — scan once at init
|
||||
{
|
||||
#ifdef __APPLE__
|
||||
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||
#else
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
#endif
|
||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||
if (!rcxFiles.isEmpty()) {
|
||||
auto* examples = file->addMenu("E&xamples");
|
||||
@@ -543,15 +595,20 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
||||
|
||||
// Edit
|
||||
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
||||
auto* edit = m_menuBar->addMenu("&Edit");
|
||||
Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
||||
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
||||
|
||||
// View
|
||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||
auto* view = m_menuBar->addMenu("&View");
|
||||
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
||||
Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
m_removeSplitAction = Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
m_removeSplitAction->setVisible(false);
|
||||
view->addSeparator();
|
||||
connect(view, &QMenu::aboutToShow, this, [this]() {
|
||||
auto* tab = activeTab();
|
||||
m_removeSplitAction->setVisible(tab && tab->panes.size() > 1);
|
||||
});
|
||||
m_sourceMenu = view->addMenu("&Data Source");
|
||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
||||
view->addSeparator();
|
||||
@@ -619,7 +676,7 @@ void MainWindow::createMenus() {
|
||||
}
|
||||
|
||||
// Tools
|
||||
auto* tools = m_titleBar->menuBar()->addMenu("&Tools");
|
||||
auto* tools = m_menuBar->addMenu("&Tools");
|
||||
Qt5Qt6AddAction(tools, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||
tools->addSeparator();
|
||||
const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||
@@ -628,11 +685,11 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||
|
||||
// Plugins
|
||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||
auto* plugins = m_menuBar->addMenu("&Plugins");
|
||||
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
||||
|
||||
// Help
|
||||
auto* help = m_titleBar->menuBar()->addMenu("&Help");
|
||||
auto* help = m_menuBar->addMenu("&Help");
|
||||
Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about);
|
||||
}
|
||||
|
||||
@@ -1065,28 +1122,53 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
setupRenderedSci(pane.rendered);
|
||||
rvLayout->addWidget(pane.rendered);
|
||||
|
||||
// Find bar (hidden by default)
|
||||
// Find bar with prev/next buttons (hidden by default)
|
||||
pane.findContainer = new QWidget;
|
||||
auto* fcLayout = new QHBoxLayout(pane.findContainer);
|
||||
fcLayout->setContentsMargins(4, 0, 0, 0);
|
||||
fcLayout->setSpacing(2);
|
||||
const auto& fbTheme = ThemeManager::instance().current();
|
||||
auto* ccPrevBtn = new QToolButton;
|
||||
ccPrevBtn->setText(QStringLiteral("\u25C0"));
|
||||
ccPrevBtn->setFixedSize(24, 24);
|
||||
auto* ccNextBtn = new QToolButton;
|
||||
ccNextBtn->setText(QStringLiteral("\u25B6"));
|
||||
ccNextBtn->setFixedSize(24, 24);
|
||||
auto* ccCloseBtn = new QToolButton;
|
||||
ccCloseBtn->setText(QStringLiteral("\u2715"));
|
||||
ccCloseBtn->setFixedSize(24, 24);
|
||||
QString btnCss = QStringLiteral(
|
||||
"QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
||||
"QToolButton:hover { background: %4; }"
|
||||
"QToolButton:pressed { background: %5; }")
|
||||
.arg(fbTheme.background.name(), fbTheme.text.name(), fbTheme.border.name(),
|
||||
fbTheme.hover.name(), fbTheme.backgroundAlt.name());
|
||||
ccPrevBtn->setStyleSheet(btnCss);
|
||||
ccNextBtn->setStyleSheet(btnCss);
|
||||
ccCloseBtn->setStyleSheet(btnCss);
|
||||
pane.findBar = new QLineEdit;
|
||||
pane.findBar->setPlaceholderText("Find...");
|
||||
pane.findBar->setVisible(false);
|
||||
const auto& fbTheme = ThemeManager::instance().current();
|
||||
pane.findBar->setStyleSheet(
|
||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" padding: 4px 8px; font-size: 13px; }")
|
||||
.arg(fbTheme.backgroundAlt.name())
|
||||
.arg(fbTheme.text.name())
|
||||
.arg(fbTheme.border.name()));
|
||||
rvLayout->addWidget(pane.findBar);
|
||||
.arg(fbTheme.backgroundAlt.name(), fbTheme.text.name(), fbTheme.border.name()));
|
||||
fcLayout->addWidget(ccPrevBtn);
|
||||
fcLayout->addWidget(ccNextBtn);
|
||||
fcLayout->addWidget(ccCloseBtn);
|
||||
fcLayout->addWidget(pane.findBar);
|
||||
pane.findContainer->setVisible(false);
|
||||
rvLayout->addWidget(pane.findContainer);
|
||||
|
||||
// Ctrl+F to show find bar
|
||||
QsciScintilla* sci = pane.rendered;
|
||||
QLineEdit* fb = pane.findBar;
|
||||
QWidget* fc = pane.findContainer;
|
||||
auto* findAction = new QAction(pane.renderedContainer);
|
||||
findAction->setShortcut(QKeySequence::Find);
|
||||
findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
pane.renderedContainer->addAction(findAction);
|
||||
connect(findAction, &QAction::triggered, fb, [fb, sci]() {
|
||||
fb->setVisible(true);
|
||||
connect(findAction, &QAction::triggered, fb, [fb, fc]() {
|
||||
fc->setVisible(true);
|
||||
fb->setFocus();
|
||||
fb->selectAll();
|
||||
});
|
||||
@@ -1096,8 +1178,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
||||
escAction->setShortcutContext(Qt::WidgetShortcut);
|
||||
fb->addAction(escAction);
|
||||
connect(escAction, &QAction::triggered, fb, [fb, sci]() {
|
||||
fb->setVisible(false);
|
||||
connect(escAction, &QAction::triggered, fb, [fc, sci]() {
|
||||
fc->setVisible(false);
|
||||
sci->setFocus();
|
||||
});
|
||||
|
||||
@@ -1112,6 +1194,21 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
if (!sci->findNext())
|
||||
sci->findFirst(text, false, false, false, true, true, 0, 0);
|
||||
});
|
||||
connect(ccNextBtn, &QToolButton::clicked, sci, [sci, fb]() {
|
||||
if (!sci->findNext())
|
||||
sci->findFirst(fb->text(), false, false, false, true, true, 0, 0);
|
||||
});
|
||||
connect(ccPrevBtn, &QToolButton::clicked, sci, [sci, fb]() {
|
||||
QString text = fb->text();
|
||||
if (text.isEmpty()) return;
|
||||
int line, col;
|
||||
sci->getCursorPosition(&line, &col);
|
||||
sci->findFirst(text, false, false, false, true, false, line, col);
|
||||
});
|
||||
connect(ccCloseBtn, &QToolButton::clicked, sci, [fc, sci]() {
|
||||
fc->setVisible(false);
|
||||
sci->setFocus();
|
||||
});
|
||||
|
||||
pane.tabWidget->addTab(pane.renderedContainer, "C/C++"); // index 1
|
||||
|
||||
@@ -1342,52 +1439,6 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,57 +1567,11 @@ static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef Q_OS_WIN
|
||||
// Create a new project, then point it at the live editor object
|
||||
// Tab 2: Editor demo with live process memory (created first)
|
||||
project_new();
|
||||
|
||||
auto* ctrl = activeController();
|
||||
@@ -1583,8 +1588,14 @@ void MainWindow::selfTest() {
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
|
||||
// Tab 1: Empty class for user work (created second, becomes active)
|
||||
auto* userTab = project_new(QStringLiteral("class"));
|
||||
m_mdiArea->setActiveSubWindow(userTab);
|
||||
#else
|
||||
project_new();
|
||||
auto* userTab = project_new(QStringLiteral("class"));
|
||||
m_mdiArea->setActiveSubWindow(userTab);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1750,10 +1761,15 @@ void MainWindow::toggleMcp() {
|
||||
void MainWindow::applyTheme(const Theme& theme) {
|
||||
applyGlobalTheme(theme);
|
||||
|
||||
#ifdef __APPLE__
|
||||
applyMacTitleBarTheme(this, theme);
|
||||
#endif
|
||||
|
||||
// Dock separator is 1px via PM_DockWidgetSeparatorExtent in MenuBarStyle
|
||||
|
||||
// Custom title bar
|
||||
m_titleBar->applyTheme(theme);
|
||||
if (m_titleBar)
|
||||
m_titleBar->applyTheme(theme);
|
||||
|
||||
// Update border overlay color
|
||||
updateBorderColor(isActiveWindow() ? theme.borderFocused : theme.border);
|
||||
@@ -1910,8 +1926,10 @@ void MainWindow::showOptionsDialog() {
|
||||
OptionsResult current;
|
||||
current.themeIndex = tm.currentIndex();
|
||||
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||
current.menuBarTitleCase = m_menuBarTitleCase;
|
||||
current.showIcon = m_titleBar
|
||||
? QSettings("Reclass", "Reclass").value("showIcon", false).toBool()
|
||||
: false;
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
@@ -1929,12 +1947,13 @@ void MainWindow::showOptionsDialog() {
|
||||
setEditorFont(r.fontName);
|
||||
|
||||
if (r.menuBarTitleCase != current.menuBarTitleCase) {
|
||||
m_titleBar->setMenuBarTitleCase(r.menuBarTitleCase);
|
||||
applyMenuBarTitleCase(r.menuBarTitleCase);
|
||||
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
||||
}
|
||||
|
||||
if (r.showIcon != current.showIcon) {
|
||||
m_titleBar->setShowIcon(r.showIcon);
|
||||
if (m_titleBar)
|
||||
m_titleBar->setShowIcon(r.showIcon);
|
||||
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
||||
}
|
||||
|
||||
@@ -2011,6 +2030,9 @@ MainWindow::TabState* MainWindow::tabByIndex(int index) {
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle() {
|
||||
#ifdef __APPLE__
|
||||
setWindowTitle(QStringLiteral("Reclass"));
|
||||
#else
|
||||
QString title;
|
||||
auto* sub = m_mdiArea->activeSubWindow();
|
||||
if (sub && m_tabs.contains(sub)) {
|
||||
@@ -2024,6 +2046,7 @@ void MainWindow::updateWindowTitle() {
|
||||
title = "Reclass";
|
||||
}
|
||||
setWindowTitle(title);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── Rendered view setup ──
|
||||
@@ -2566,6 +2589,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
addRecentFile(filePath);
|
||||
return sub;
|
||||
}
|
||||
|
||||
@@ -2582,6 +2606,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
addRecentFile(filePath);
|
||||
return sub;
|
||||
}
|
||||
|
||||
@@ -2595,8 +2620,10 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
|
||||
"Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)");
|
||||
if (path.isEmpty()) return false;
|
||||
tab.doc->save(path);
|
||||
addRecentFile(path);
|
||||
} else {
|
||||
tab.doc->save(tab.doc->filePath);
|
||||
addRecentFile(tab.doc->filePath);
|
||||
}
|
||||
updateWindowTitle();
|
||||
return true;
|
||||
@@ -2812,6 +2839,18 @@ void MainWindow::createWorkspaceDock() {
|
||||
}
|
||||
});
|
||||
|
||||
// Ctrl+F focuses the workspace search field
|
||||
{
|
||||
auto* findAction = new QAction(dockContainer);
|
||||
findAction->setShortcut(QKeySequence::Find);
|
||||
findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
dockContainer->addAction(findAction);
|
||||
connect(findAction, &QAction::triggered, this, [this]() {
|
||||
m_workspaceSearch->setFocus();
|
||||
m_workspaceSearch->selectAll();
|
||||
});
|
||||
}
|
||||
|
||||
m_workspaceDock->setWidget(dockContainer);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||
m_workspaceDock->hide();
|
||||
@@ -2967,6 +3006,43 @@ void MainWindow::rebuildWorkspaceModel() {
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
}
|
||||
|
||||
void MainWindow::addRecentFile(const QString& path) {
|
||||
if (path.isEmpty()) return;
|
||||
QString absPath = QFileInfo(path).absoluteFilePath();
|
||||
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QStringList recent = s.value("recentFiles").toStringList();
|
||||
recent.removeAll(absPath);
|
||||
recent.prepend(absPath);
|
||||
while (recent.size() > 10)
|
||||
recent.removeLast();
|
||||
s.setValue("recentFiles", recent);
|
||||
|
||||
updateRecentFilesMenu();
|
||||
}
|
||||
|
||||
void MainWindow::updateRecentFilesMenu() {
|
||||
if (!m_recentFilesMenu) return;
|
||||
m_recentFilesMenu->clear();
|
||||
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QStringList recent = s.value("recentFiles").toStringList();
|
||||
|
||||
int added = 0;
|
||||
for (const QString& path : recent) {
|
||||
if (!QFile::exists(path)) continue;
|
||||
QString label = QStringLiteral("&%1 %2").arg(added + 1).arg(QFileInfo(path).fileName());
|
||||
m_recentFilesMenu->addAction(label, this, [this, path]() {
|
||||
project_open(path);
|
||||
})->setToolTip(path);
|
||||
if (++added >= 10) break;
|
||||
}
|
||||
if (added == 0) {
|
||||
auto* empty = m_recentFilesMenu->addAction(QStringLiteral("(empty)"));
|
||||
empty->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::populateSourceMenu() {
|
||||
m_sourceMenu->clear();
|
||||
auto* ctrl = activeController();
|
||||
@@ -3136,7 +3212,7 @@ void MainWindow::changeEvent(QEvent* event) {
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
updateBorderColor(isActiveWindow() ? t.borderFocused : t.border);
|
||||
}
|
||||
if (event->type() == QEvent::WindowStateChange)
|
||||
if (event->type() == QEvent::WindowStateChange && m_titleBar)
|
||||
m_titleBar->updateMaximizeIcon();
|
||||
}
|
||||
|
||||
@@ -3166,6 +3242,9 @@ int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
SetUnhandledExceptionFilter(crashHandler);
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
|
||||
#endif
|
||||
|
||||
DarkApp app(argc, argv);
|
||||
app.setApplicationName("Reclass");
|
||||
|
||||
@@ -88,17 +88,22 @@ private:
|
||||
QPushButton* m_btnReclass = nullptr;
|
||||
QPushButton* m_btnRendered = nullptr;
|
||||
TitleBarWidget* m_titleBar = nullptr;
|
||||
QMenuBar* m_menuBar = nullptr;
|
||||
bool m_menuBarTitleCase = false;
|
||||
QWidget* m_borderOverlay = nullptr;
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QAction* m_removeSplitAction = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
QMenu* m_recentFilesMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
RcxEditor* editor = nullptr;
|
||||
QsciScintilla* rendered = nullptr;
|
||||
QLineEdit* findBar = nullptr;
|
||||
QWidget* findContainer = nullptr;
|
||||
QWidget* renderedContainer = nullptr;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
@@ -116,9 +121,12 @@ private:
|
||||
void rebuildAllDocs();
|
||||
|
||||
void createMenus();
|
||||
void applyMenuBarTitleCase(bool titleCase);
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
void addRecentFile(const QString& path);
|
||||
void updateRecentFilesMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
|
||||
@@ -256,7 +256,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. "
|
||||
"change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
|
||||
"change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000'}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. "
|
||||
"change_struct_type: {op:'change_struct_type', nodeId:'ID', structTypeName:'Name'}. "
|
||||
"change_class_keyword: {op:'change_class_keyword', nodeId:'ID', classKeyword:'class'}. "
|
||||
"change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
|
||||
@@ -396,6 +396,24 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
// 9. node.history
|
||||
tools.append(QJsonObject{
|
||||
{"name", "node.history"},
|
||||
{"description", "Returns timestamped value change history (up to 10 entries) "
|
||||
"for specified nodes. Requires live provider with value tracking enabled."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"nodeIds", QJsonObject{{"type", "array"},
|
||||
{"items", QJsonObject{{"type", "string"}}},
|
||||
{"description", "Array of node IDs to get history for."}}},
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index. Omit for active tab."}}}
|
||||
}},
|
||||
{"required", QJsonArray{"nodeIds"}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -420,6 +438,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else if (toolName == "node.history") result = toolNodeHistory(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
@@ -751,8 +770,10 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
}
|
||||
else if (opType == "change_base") {
|
||||
uint64_t newBase = op.value("baseAddress").toString().toULongLong(nullptr, 16);
|
||||
QString oldFormula = tree.baseAddressFormula;
|
||||
QString newFormula = op.value("formula").toString();
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeBase{tree.baseAddress, newBase}));
|
||||
cmd::ChangeBase{tree.baseAddress, newBase, oldFormula, newFormula}));
|
||||
applied++;
|
||||
}
|
||||
else if (opType == "change_struct_type") {
|
||||
@@ -1226,6 +1247,43 @@ QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
|
||||
QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Tool: node.history — return timestamped value history for nodes
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab.", true);
|
||||
|
||||
const auto& histMap = tab->ctrl->valueHistory();
|
||||
QJsonArray requestedIds = args.value("nodeIds").toArray();
|
||||
if (requestedIds.isEmpty())
|
||||
return makeTextResult("nodeIds array is required.", true);
|
||||
|
||||
QJsonObject result;
|
||||
for (const auto& idVal : requestedIds) {
|
||||
QString idStr = idVal.toString();
|
||||
uint64_t nodeId = idStr.toULongLong();
|
||||
auto it = histMap.find(nodeId);
|
||||
QJsonArray entries;
|
||||
if (it != histMap.end()) {
|
||||
it->forEachWithTime([&](const QString& val, qint64 msec) {
|
||||
QJsonObject entry;
|
||||
entry.insert(QStringLiteral("value"), val);
|
||||
entry.insert(QStringLiteral("timestamp"), msec);
|
||||
entries.append(entry);
|
||||
});
|
||||
}
|
||||
QJsonObject nodeResult;
|
||||
nodeResult.insert(QStringLiteral("entries"), entries);
|
||||
nodeResult.insert(QStringLiteral("heatLevel"), it != histMap.end() ? it->heatLevel() : 0);
|
||||
nodeResult.insert(QStringLiteral("uniqueCount"), it != histMap.end() ? it->uniqueCount() : 0);
|
||||
result.insert(idStr, nodeResult);
|
||||
}
|
||||
return makeTextResult(QString::fromUtf8(
|
||||
QJsonDocument(result).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -59,6 +59,7 @@ private:
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QMenu>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -27,22 +31,9 @@ ProcessPicker::ProcessPicker(QWidget *parent)
|
||||
, m_useCustomList(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column - fixed width
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column - fixed width
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true); // Path column - fills remaining space
|
||||
ui->processTable->setWordWrap(false); // Disable word wrap for single-line display
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft); // Elide from left (show end of path)
|
||||
|
||||
// Connect signals
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Initial process enumeration
|
||||
initUi();
|
||||
refreshProcessList();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget *parent)
|
||||
@@ -51,23 +42,102 @@ ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget
|
||||
, m_useCustomList(true)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80);
|
||||
ui->processTable->setColumnWidth(1, 200);
|
||||
initUi();
|
||||
ui->refreshButton->setVisible(false);
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
void ProcessPicker::initUi()
|
||||
{
|
||||
// Table configuration
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
||||
ui->processTable->setWordWrap(false);
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
||||
|
||||
// Connect signals (no refresh button for custom lists)
|
||||
ui->refreshButton->setVisible(false);
|
||||
ui->processTable->setShowGrid(false);
|
||||
ui->processTable->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 6);
|
||||
|
||||
// Signal connections
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Use custom process list
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
|
||||
// Derive theme colors from the global palette (set by applyGlobalTheme)
|
||||
QPalette pal = qApp->palette();
|
||||
QString bg = pal.color(QPalette::Base).name();
|
||||
QString text = pal.color(QPalette::Text).name();
|
||||
QString hover = pal.color(QPalette::Mid).name();
|
||||
QString surface = pal.color(QPalette::AlternateBase).name();
|
||||
QString button = pal.color(QPalette::Button).name();
|
||||
QString highlight= pal.color(QPalette::Highlight).name();
|
||||
QString border = pal.color(QPalette::Mid).darker(120).name();
|
||||
QString mutedText= pal.color(QPalette::Disabled, QPalette::WindowText).name();
|
||||
QString hoverDk = pal.color(QPalette::Mid).darker(130).name();
|
||||
|
||||
ui->processTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }")
|
||||
.arg(bg, text, hover));
|
||||
|
||||
ui->processTable->horizontalHeader()->setStyleSheet(QStringLiteral(
|
||||
"QHeaderView::section { background: %1; color: %2; border: none;"
|
||||
" padding: 4px 6px; text-align: left; }")
|
||||
.arg(surface, text));
|
||||
ui->processTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
ui->filterEdit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(bg, text, border, highlight));
|
||||
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(button, text, border, hover, hoverDk, mutedText);
|
||||
ui->refreshButton->setStyleSheet(btnStyle);
|
||||
ui->attachButton->setStyleSheet(btnStyle);
|
||||
ui->cancelButton->setStyleSheet(btnStyle);
|
||||
|
||||
// Right-click context menu
|
||||
ui->processTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(ui->processTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = ui->processTable->rowAt(pos.y());
|
||||
if (row < 0) return;
|
||||
auto* pidItem = ui->processTable->item(row, 0);
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
auto* pathItem = ui->processTable->item(row, 2);
|
||||
if (!pidItem || !nameItem) return;
|
||||
|
||||
QString pid = QString::number(pidItem->data(Qt::EditRole).toUInt());
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
QString path = pathItem ? pathItem->text() : QString();
|
||||
|
||||
QMenu menu;
|
||||
auto* copyPid = menu.addAction(QStringLiteral("Copy PID"));
|
||||
auto* copyName = menu.addAction(QStringLiteral("Copy Name"));
|
||||
QAction* copyPath = nullptr;
|
||||
if (!path.isEmpty())
|
||||
copyPath = menu.addAction(QStringLiteral("Copy Path"));
|
||||
|
||||
auto* chosen = menu.exec(ui->processTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyPid)
|
||||
QApplication::clipboard()->setText(pid);
|
||||
else if (chosen == copyName)
|
||||
QApplication::clipboard()->setText(name);
|
||||
else if (copyPath && chosen == copyPath)
|
||||
QApplication::clipboard()->setText(path);
|
||||
});
|
||||
|
||||
// Auto-focus filter for immediate typing
|
||||
ui->filterEdit->setFocus();
|
||||
}
|
||||
|
||||
ProcessPicker::~ProcessPicker()
|
||||
@@ -97,31 +167,31 @@ void ProcessPicker::onProcessSelected()
|
||||
{
|
||||
auto* item = ui->processTable->currentItem();
|
||||
if (!item) return;
|
||||
|
||||
|
||||
int row = item->row();
|
||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
||||
// Use original name stored in UserRole (without architecture suffix)
|
||||
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
|
||||
m_selectedName = origName.isValid() ? origName.toString()
|
||||
: ui->processTable->item(row, 1)->text();
|
||||
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
QList<ProcessInfo> processes;
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
QMessageBox::warning(this, "Error", "Failed to enumerate processes.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
|
||||
if (Process32FirstW(snapshot, &pe32))
|
||||
{
|
||||
do
|
||||
@@ -129,10 +199,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
ProcessInfo info;
|
||||
info.pid = pe32.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(pe32.szExeFile);
|
||||
|
||||
// Try to get full path and extract icon
|
||||
// If we can't open a process with PROCESS_QUERY_LIMITED_INFORMATION then
|
||||
// we for sure can't access their memory. - Skip in this case
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
|
||||
if (hProcess)
|
||||
{
|
||||
@@ -143,7 +210,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
GetModuleFileNameExW(hProcess, nullptr, path, pathLen))
|
||||
{
|
||||
info.path = QString::fromWCharArray(path);
|
||||
|
||||
|
||||
// Extract icon from executable
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
|
||||
@@ -292,3 +359,22 @@ void ProcessPicker::applyFilter()
|
||||
|
||||
populateTable(filtered);
|
||||
}
|
||||
|
||||
void ProcessPicker::selectPreferredProcess()
|
||||
{
|
||||
// Try to select the last-attached process if it's in the list
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString lastProc = s.value("lastAttachedProcess").toString();
|
||||
if (lastProc.isEmpty()) return;
|
||||
|
||||
for (int row = 0; row < ui->processTable->rowCount(); ++row) {
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
if (!nameItem) continue;
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
if (name.compare(lastProc, Qt::CaseInsensitive) == 0) {
|
||||
ui->processTable->selectRow(row);
|
||||
ui->processTable->scrollToItem(nameItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ private slots:
|
||||
void filterProcesses(const QString& text);
|
||||
|
||||
private:
|
||||
void initUi();
|
||||
void enumerateProcesses();
|
||||
void populateTable(const QList<ProcessInfo>& processes);
|
||||
void applyFilter();
|
||||
void selectPreferredProcess();
|
||||
|
||||
Ui::ProcessPicker *ui;
|
||||
uint32_t m_selectedPid = 0;
|
||||
|
||||
@@ -127,22 +127,6 @@
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>attachButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ProcessPicker</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>600</x>
|
||||
<y>470</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>350</x>
|
||||
<y>250</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>cancelButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
|
||||
@@ -33,7 +33,12 @@ ThemeManager::ThemeManager() {
|
||||
// ── Load built-in themes from JSON files next to the executable ──
|
||||
|
||||
QString ThemeManager::builtInDir() const {
|
||||
#ifdef Q_OS_MACOS
|
||||
// In a macOS .app bundle, resources live in Contents/Resources, not Contents/MacOS
|
||||
return QCoreApplication::applicationDirPath() + "/../Resources/themes";
|
||||
#else
|
||||
return QCoreApplication::applicationDirPath() + "/themes";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ThemeManager::loadBuiltInThemes() {
|
||||
|
||||
@@ -74,7 +74,7 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
// App label
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
.arg(theme.text.name()));
|
||||
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
@@ -82,7 +82,7 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.text);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(m_theme.textDim.name()));
|
||||
.arg(m_theme.text.name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,17 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
// Sort structs by children count descending (most fields first)
|
||||
auto cmpChildren = [&](const Entry& a, const Entry& b) {
|
||||
int ca = a.tree->childrenOf(a.node->id).size();
|
||||
int cb = b.tree->childrenOf(b.node->id).size();
|
||||
if (ca != cb) return ca > cb;
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpChildren);
|
||||
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
// Helper: type display string for a member node
|
||||
|
||||
@@ -815,6 +815,68 @@ private slots:
|
||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
||||
}
|
||||
|
||||
// ── Test: clearing value history actually resets heat to 0 ──
|
||||
void testClearValueHistoryResetsHeat() {
|
||||
// Use a live provider so value tracking runs during refresh()
|
||||
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0);
|
||||
m_ctrl->setTrackValues(true);
|
||||
|
||||
// Do initial refresh to populate m_lastResult.meta
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find field_u32 nodeId
|
||||
uint64_t targetId = 0;
|
||||
for (const auto& n : m_doc->tree.nodes) {
|
||||
if (n.name == "field_u32") { targetId = n.id; break; }
|
||||
}
|
||||
QVERIFY(targetId != 0);
|
||||
|
||||
// Seed value history with multiple changes to get heat > 0
|
||||
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||
history[targetId].record("val_1");
|
||||
history[targetId].record("val_2");
|
||||
history[targetId].record("val_3");
|
||||
QVERIFY2(history[targetId].heatLevel() >= 2,
|
||||
"Pre-clear: should have heat >= 2 (warm)");
|
||||
|
||||
// Refresh so heatLevel propagates to LineMeta
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify heat is visible in meta
|
||||
bool foundHot = false;
|
||||
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||
if (lm.nodeId == targetId && lm.heatLevel > 0) {
|
||||
foundHot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundHot, "Pre-clear: LineMeta should show heat > 0");
|
||||
|
||||
// Now simulate what the "Clear Value History" context menu does:
|
||||
// remove from history map + clear subtree + refresh
|
||||
history.remove(targetId);
|
||||
for (int ci : m_doc->tree.subtreeIndices(targetId))
|
||||
history.remove(m_doc->tree.nodes[ci].id);
|
||||
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// After clear + refresh, heatLevel must be 0 for this node
|
||||
for (const auto& lm : m_ctrl->lastResult().meta) {
|
||||
if (lm.nodeId == targetId) {
|
||||
QCOMPARE(lm.heatLevel, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The history entry should exist again (re-recorded by refresh)
|
||||
// but with only 1 unique value → heatLevel 0
|
||||
QVERIFY(history.contains(targetId));
|
||||
QCOMPARE(history[targetId].heatLevel(), 0);
|
||||
QCOMPARE(history[targetId].uniqueCount(), 1);
|
||||
}
|
||||
|
||||
void testStaticFieldTypeChangePreservesFlags() {
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user