Compare commits

...

8 Commits

Author SHA1 Message Date
IChooseYou
86499e58ee fix: remove value history cooldown hack, dismiss popup on clear
The cooldown suppressed tracking for ~1s but the popup persisted showing
stale "1h ago" values because applyDocument skips popup dismissal.
Replaced with explicit dismissHistoryPopup() after clear+refresh so the
popup is gone immediately. Value tracking resumes on the next async cycle
with a clean baseline (m_refreshGen++ discards in-flight reads,
m_prevPages.clear() prevents phantom diffs).
2026-03-03 08:38:08 -07:00
IChooseYou
b2ae8d5a5d fix: insert above node, clear value history cooldown, search context menu
- Insert 4/8 now inserts above the right-clicked node and shifts siblings
  down instead of appending at end. Insert key shortcut (Shift+Ins = 4,
  Ins = 8). Falls back to append when clicking empty space.
- Clear Value History uses a 5-cycle cooldown counter so heat stays gone
  for ~1s instead of returning on the next async refresh.
- Right-click Search defers showFindBar via QTimer::singleShot so focus
  isn't stolen by the closing context menu.
2026-03-03 08:31:49 -07:00
IChooseYou
6768f04e9a Merge pull request #6 from LabGuy94/add-macos-support
Fix file opening on macOS
2026-03-03 08:31:25 -07:00
Lab
c6e5f6508f Fix file opening on macOS 2026-03-02 15:25:57 -08:00
IChooseYou
e6529052b3 fix: clear value history clears subtree, add Copy Line and Search to context menu
- Clear Value History now removes history for all descendant nodes too
- Add "Copy Line" right-click menu item
- Add "Search..." right-click menu item (opens Ctrl+F find bar)
- Move showFindBar() to public in editor.h
2026-03-02 15:34:37 -07:00
IChooseYou
d43e989992 Merge pull request #5 from LabGuy94/add-macos-support
Add macOS support and CI
2026-03-02 14:57:55 -07:00
IChooseYou
879e9f4047 fix: global blue highlight, Ctrl+F find bar with prev/next/close buttons
- Change QPalette::Highlight from theme.selection to theme.hover globally
- RcxEditor find: use SCI_SEARCHINTARGET + INDIC_COMPOSITIONTHICK indicator
  (selection rendering is disabled, so findFirst was invisible)
- Re-apply find indicators after applyDocument() refresh cycle
- Add prev/next/close buttons to find bars in both Reclass and C/C++ modes
- Buttons styled with hover/pressed states matching tab styling
2026-03-02 14:53:14 -07:00
Lab
e0d5a799b4 Add macOS support and CI 2026-03-02 11:34:22 -08:00
18 changed files with 900 additions and 180 deletions

View File

@@ -3,7 +3,7 @@ name: Build
on:
push:
branches:
- '**'
- "**"
pull_request:
branches: [main]
@@ -22,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
@@ -84,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
@@ -141,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:
@@ -168,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
View File

@@ -14,3 +14,4 @@ CMakeUserPresets.json
plugins/RcNetPluginCompatLayer/bridge/obj
plugins/RcNetPluginCompatLayer/bridge/bin
.cache
*.DS_Store

View File

@@ -113,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
@@ -124,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
@@ -150,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")
@@ -158,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)
@@ -298,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
@@ -477,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)

View File

@@ -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 wont 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)

168
scripts/build_macos.sh Executable file
View 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

View File

@@ -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;
@@ -1636,11 +1703,16 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
connect(act, &QAction::triggered, this, [this, ids]() {
for (uint64_t id : ids) {
m_valueHistory[id].clear();
for (auto& lm : m_lastResult.meta)
if (lm.nodeId == id) lm.heatLevel = 0;
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();
@@ -1672,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);
@@ -1689,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];
@@ -1833,12 +1928,16 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
{
auto* act = menu.addAction("Clear Value History");
act->setToolTip(QStringLiteral("Reset change tracking for this node"));
act->setEnabled(m_valueHistory.contains(nodeId) && m_valueHistory[nodeId].uniqueCount() > 0);
connect(act, &QAction::triggered, this, [this, nodeId]() {
m_valueHistory[nodeId].clear();
for (auto& lm : m_lastResult.meta)
if (lm.nodeId == nodeId) lm.heatLevel = 0;
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();
@@ -1990,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)
}
@@ -2088,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);
}

View File

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

View File

@@ -545,13 +545,12 @@ struct ValueHistory {
fn(values[(start + i) % kCapacity]);
}
// Iterate with timestamps from oldest to newest
// Iterate with timestamps from newest to oldest
template<typename Fn>
void forEachWithTime(Fn&& fn) const {
int n = uniqueCount();
int start = (head + kCapacity - n) % kCapacity;
for (int i = 0; i < n; i++) {
int idx = (start + i) % kCapacity;
int idx = (head + kCapacity - 1 - i) % kCapacity;
fn(values[idx], timestamps[idx]);
}
}

View File

@@ -385,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";
@@ -402,10 +403,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
layout->addWidget(m_sci);
// Find bar (hidden by default, shown with Ctrl+F)
m_findBar = new QLineEdit(this);
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..."));
m_findBar->setVisible(false);
layout->addWidget(m_findBar);
fbLayout->addWidget(findPrevBtn);
fbLayout->addWidget(findNextBtn);
fbLayout->addWidget(findCloseBtn);
fbLayout->addWidget(m_findBar);
m_findBarContainer->setVisible(false);
layout->addWidget(m_findBarContainer);
setupScintilla();
setupLexer();
@@ -422,18 +440,46 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
m_sci->viewport()->installEventFilter(this);
m_sci->viewport()->setMouseTracking(true);
// Find bar: live search on text change
connect(m_findBar, &QLineEdit::textChanged, this, [this](const QString& text) {
if (text.isEmpty()) return;
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
});
// Find bar: Enter jumps to next match (wraps at end)
connect(m_findBar, &QLineEdit::returnPressed, this, [this]() {
// 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;
if (!m_sci->findNext())
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
});
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);
@@ -646,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() {
@@ -782,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);
@@ -832,11 +886,17 @@ void RcxEditor::applyTheme(const Theme& theme) {
}
// Find bar
if (m_findBar) {
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()));
}
}
@@ -919,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) {
@@ -1300,13 +1381,23 @@ int RcxEditor::currentNodeIndex() const {
}
void RcxEditor::showFindBar() {
m_findBar->setVisible(true);
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_findBar->setVisible(false);
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();
}
@@ -1385,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)];
@@ -2156,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,

View File

@@ -33,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);
@@ -79,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;
@@ -156,8 +159,9 @@ private:
const NodeTree* m_disasmTree = nullptr;
// ── Find bar ──
QWidget* m_findBarContainer = nullptr;
QLineEdit* m_findBar = nullptr;
void showFindBar();
long m_findPos = 0;
void hideFindBar();
// ── Reentrancy guards ──

BIN
src/icons/class.icns Normal file

Binary file not shown.

13
src/macos_titlebar.h Normal file
View 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
View 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

View File

@@ -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,9 +520,42 @@ 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);
@@ -529,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");
@@ -545,12 +595,12 @@ 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);
m_removeSplitAction = Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
m_removeSplitAction->setVisible(false);
@@ -626,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";
@@ -635,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);
}
@@ -1072,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();
});
@@ -1103,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();
});
@@ -1119,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
@@ -1671,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);
@@ -1831,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();
@@ -1850,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);
}
@@ -1932,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)) {
@@ -1945,6 +2046,7 @@ void MainWindow::updateWindowTitle() {
title = "Reclass";
}
setWindowTitle(title);
#endif
}
// ── Rendered view setup ──
@@ -3110,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();
}
@@ -3140,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");

View File

@@ -88,6 +88,8 @@ 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;
@@ -101,6 +103,7 @@ private:
RcxEditor* editor = nullptr;
QsciScintilla* rendered = nullptr;
QLineEdit* findBar = nullptr;
QWidget* findContainer = nullptr;
QWidget* renderedContainer = nullptr;
ViewMode viewMode = VM_Reclass;
uint64_t lastRenderedRootId = 0;
@@ -118,6 +121,7 @@ private:
void rebuildAllDocs();
void createMenus();
void applyMenuBarTitleCase(bool titleCase);
void createStatusBar();
void showPluginsDialog();
void populateSourceMenu();

View File

@@ -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() {

View File

@@ -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()));
}
}

View File

@@ -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;