mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
8 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86499e58ee | ||
|
|
b2ae8d5a5d | ||
|
|
6768f04e9a | ||
|
|
c6e5f6508f | ||
|
|
e6529052b3 | ||
|
|
d43e989992 | ||
|
|
879e9f4047 | ||
|
|
e0d5a799b4 |
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ jobs:
|
|||||||
- name: Install Qt6 and MinGW
|
- name: Install Qt6 and MinGW
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
version: '6.8.1'
|
version: "6.8.1"
|
||||||
arch: 'win64_mingw'
|
arch: "win64_mingw"
|
||||||
tools: 'tools_mingw1310,qt.tools.win64_mingw1310'
|
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
- name: Install Qt6
|
- name: Install Qt6
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
version: '6.8.1'
|
version: "6.8.1"
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -141,9 +141,66 @@ jobs:
|
|||||||
name: Reclass-linux64-qt6
|
name: Reclass-linux64-qt6
|
||||||
path: Reclass-linux64-qt6.AppImage
|
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:
|
release:
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
needs: [windows, linux]
|
needs: [windows, linux, macos]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -168,5 +225,7 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ CMakeUserPresets.json
|
|||||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||||
.cache
|
.cache
|
||||||
|
*.DS_Store
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ add_executable(Reclass
|
|||||||
src/optionsdialog.cpp
|
src/optionsdialog.cpp
|
||||||
src/titlebar.h
|
src/titlebar.h
|
||||||
src/titlebar.cpp
|
src/titlebar.cpp
|
||||||
|
src/macos_titlebar.h
|
||||||
|
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||||
src/mcp/mcp_bridge.h
|
src/mcp/mcp_bridge.h
|
||||||
src/mcp/mcp_bridge.cpp
|
src/mcp/mcp_bridge.cpp
|
||||||
src/addressparser.h
|
src/addressparser.h
|
||||||
@@ -124,6 +126,16 @@ add_executable(Reclass
|
|||||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
$<$<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_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||||
|
|
||||||
target_link_libraries(Reclass PRIVATE
|
target_link_libraries(Reclass PRIVATE
|
||||||
@@ -150,6 +162,12 @@ foreach(_tf ${_theme_files})
|
|||||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||||
endforeach()
|
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
|
# Copy example .rcx files to build directory
|
||||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||||
@@ -158,6 +176,12 @@ foreach(_ef ${_example_files})
|
|||||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
target_sources(Reclass PRIVATE ${_example_files})
|
||||||
|
set_source_files_properties(${_example_files}
|
||||||
|
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||||
|
endif()
|
||||||
|
|
||||||
include(deploy)
|
include(deploy)
|
||||||
|
|
||||||
|
|
||||||
@@ -481,8 +505,10 @@ if(BUILD_TESTING)
|
|||||||
|
|
||||||
endif() # BUILD_UI_TESTS
|
endif() # BUILD_UI_TESTS
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(plugins/ProcessMemory)
|
if(NOT APPLE)
|
||||||
add_subdirectory(plugins/RemoteProcessMemory)
|
add_subdirectory(plugins/ProcessMemory)
|
||||||
|
add_subdirectory(plugins/RemoteProcessMemory)
|
||||||
|
endif()
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_subdirectory(plugins/WinDbgMemory)
|
add_subdirectory(plugins/WinDbgMemory)
|
||||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
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
|
- [ ] Safe mode
|
||||||
- [ ] File import for other Reclass instances
|
- [ ] File import for other Reclass instances
|
||||||
- [ ] Expose UI functionality to plugins
|
- [ ] Expose UI functionality to plugins
|
||||||
- [ ] iOS/macOS support
|
- [ ] iOS support
|
||||||
- [ ] Display RTTI information
|
- [ ] Display RTTI information
|
||||||
|
|
||||||
## Data Sources
|
## Data Sources
|
||||||
@@ -92,7 +92,7 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `
|
|||||||
|
|
||||||
### Quick Build
|
### Quick Build
|
||||||
|
|
||||||
```bash
|
```/dev/null/commands.sh#L1-4
|
||||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||||
cd Reclass
|
cd Reclass
|
||||||
.\scripts\build_qscintilla.ps1
|
.\scripts\build_qscintilla.ps1
|
||||||
@@ -101,6 +101,16 @@ cd Reclass
|
|||||||
|
|
||||||
The build script auto-detects your Qt install location.
|
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)
|
### Manual Build (MinGW)
|
||||||
|
|
||||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||||
|
|||||||
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);
|
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
|
// Inline editing signals
|
||||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||||
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
|
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,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeKind{node.id, node.kind, newKind, {}}));
|
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)
|
// Insert hex nodes to fill the gap (largest first for alignment)
|
||||||
int padOffset = baseOffset;
|
int padOffset = baseOffset;
|
||||||
while (gap > 0) {
|
while (gap > 0) {
|
||||||
@@ -741,8 +761,19 @@ void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
|
|||||||
adjs.append({sib.id, sib.offset, sib.offset + delta});
|
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,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::ChangeKind{node.id, node.kind, newKind, adjs}));
|
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}));
|
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) {
|
void RcxController::removeNode(int nodeIdx) {
|
||||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||||
@@ -1558,6 +1614,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
return indices;
|
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
|
// Quick-convert shortcuts when all selected nodes share the same kind
|
||||||
NodeKind commonKind = NodeKind::Hex64;
|
NodeKind commonKind = NodeKind::Hex64;
|
||||||
bool allSame = true;
|
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"));
|
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
|
||||||
connect(act, &QAction::triggered, this, [this, ids]() {
|
connect(act, &QAction::triggered, this, [this, ids]() {
|
||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
m_valueHistory[id].clear();
|
m_valueHistory.remove(id);
|
||||||
for (auto& lm : m_lastResult.meta)
|
for (int ci : m_doc->tree.subtreeIndices(id))
|
||||||
if (lm.nodeId == id) lm.heatLevel = 0;
|
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();
|
refresh();
|
||||||
|
for (auto* editor : m_editors)
|
||||||
|
editor->dismissHistoryPopup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
@@ -1672,7 +1744,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
menu.addSeparator();
|
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;
|
QStringList addrs;
|
||||||
for (uint64_t id : ids) {
|
for (uint64_t id : ids) {
|
||||||
int ni = m_doc->tree.indexOfId(id);
|
int ni = m_doc->tree.indexOfId(id);
|
||||||
@@ -1689,6 +1762,28 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
QMenu menu;
|
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) ──
|
// ── Node-specific actions (only when clicking on a node) ──
|
||||||
if (hasNode) {
|
if (hasNode) {
|
||||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
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");
|
auto* act = menu.addAction("Clear Value History");
|
||||||
act->setToolTip(QStringLiteral("Reset change tracking for this node"));
|
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]() {
|
connect(act, &QAction::triggered, this, [this, nodeId]() {
|
||||||
m_valueHistory[nodeId].clear();
|
m_valueHistory.remove(nodeId);
|
||||||
for (auto& lm : m_lastResult.meta)
|
for (int ci : m_doc->tree.subtreeIndices(nodeId))
|
||||||
if (lm.nodeId == nodeId) lm.heatLevel = 0;
|
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();
|
refresh();
|
||||||
|
for (auto* editor : m_editors)
|
||||||
|
editor->dismissHistoryPopup();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
@@ -1990,24 +2089,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
if (ni >= 0) removeNode(ni);
|
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();
|
menu.addSeparator();
|
||||||
} // else (non-member node actions)
|
} // else (non-member node actions)
|
||||||
}
|
}
|
||||||
@@ -2088,10 +2169,46 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
menu.addSeparator();
|
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());
|
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);
|
menu.exec(globalPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ public:
|
|||||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||||
void renameNode(int nodeIdx, const QString& newName);
|
void renameNode(int nodeIdx, const QString& newName);
|
||||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
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 removeNode(int nodeIdx);
|
||||||
void toggleCollapse(int nodeIdx);
|
void toggleCollapse(int nodeIdx);
|
||||||
void materializeRefChildren(int nodeIdx);
|
void materializeRefChildren(int nodeIdx);
|
||||||
@@ -147,8 +148,9 @@ public:
|
|||||||
// Cross-tab type visibility: point at the project's full document list
|
// Cross-tab type visibility: point at the project's full document list
|
||||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||||
|
|
||||||
// Test accessor
|
// Test accessors
|
||||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||||
|
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void nodeSelected(int nodeIdx);
|
void nodeSelected(int nodeIdx);
|
||||||
|
|||||||
@@ -545,13 +545,12 @@ struct ValueHistory {
|
|||||||
fn(values[(start + i) % kCapacity]);
|
fn(values[(start + i) % kCapacity]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate with timestamps from oldest to newest
|
// Iterate with timestamps from newest to oldest
|
||||||
template<typename Fn>
|
template<typename Fn>
|
||||||
void forEachWithTime(Fn&& fn) const {
|
void forEachWithTime(Fn&& fn) const {
|
||||||
int n = uniqueCount();
|
int n = uniqueCount();
|
||||||
int start = (head + kCapacity - n) % kCapacity;
|
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
int idx = (start + i) % kCapacity;
|
int idx = (head + kCapacity - 1 - i) % kCapacity;
|
||||||
fn(values[idx], timestamps[idx]);
|
fn(values[idx], timestamps[idx]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
src/editor.cpp
136
src/editor.cpp
@@ -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_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_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||||
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent 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";
|
static QString g_fontName = "JetBrains Mono";
|
||||||
|
|
||||||
@@ -402,10 +403,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
layout->addWidget(m_sci);
|
layout->addWidget(m_sci);
|
||||||
|
|
||||||
// Find bar (hidden by default, shown with Ctrl+F)
|
// 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->setPlaceholderText(QStringLiteral("Find..."));
|
||||||
m_findBar->setVisible(false);
|
fbLayout->addWidget(findPrevBtn);
|
||||||
layout->addWidget(m_findBar);
|
fbLayout->addWidget(findNextBtn);
|
||||||
|
fbLayout->addWidget(findCloseBtn);
|
||||||
|
fbLayout->addWidget(m_findBar);
|
||||||
|
m_findBarContainer->setVisible(false);
|
||||||
|
layout->addWidget(m_findBarContainer);
|
||||||
|
|
||||||
setupScintilla();
|
setupScintilla();
|
||||||
setupLexer();
|
setupLexer();
|
||||||
@@ -422,18 +440,46 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
m_sci->viewport()->installEventFilter(this);
|
m_sci->viewport()->installEventFilter(this);
|
||||||
m_sci->viewport()->setMouseTracking(true);
|
m_sci->viewport()->setMouseTracking(true);
|
||||||
|
|
||||||
// Find bar: live search on text change
|
// Find bar: indicator-based search (selection is disabled in our Scintilla)
|
||||||
connect(m_findBar, &QLineEdit::textChanged, this, [this](const QString& text) {
|
auto doFind = [this](bool forward) {
|
||||||
if (text.isEmpty()) return;
|
long docLen = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLENGTH);
|
||||||
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, (long)IND_FIND);
|
||||||
});
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (long)0, docLen);
|
||||||
// Find bar: Enter jumps to next match (wraps at end)
|
|
||||||
connect(m_findBar, &QLineEdit::returnPressed, this, [this]() {
|
|
||||||
QString text = m_findBar->text();
|
QString text = m_findBar->text();
|
||||||
if (text.isEmpty()) return;
|
if (text.isEmpty()) return;
|
||||||
if (!m_sci->findNext())
|
QByteArray needle = text.toUtf8();
|
||||||
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
|
|
||||||
});
|
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
|
// Escape hides find bar
|
||||||
{
|
{
|
||||||
auto* escAction = new QAction(m_findBar);
|
auto* escAction = new QAction(m_findBar);
|
||||||
@@ -646,6 +692,12 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
|
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() {
|
void RcxEditor::setupLexer() {
|
||||||
@@ -782,6 +834,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
IND_HINT_GREEN, theme.indHintGreen);
|
IND_HINT_GREEN, theme.indHintGreen);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_LOCAL_OFF, theme.textFaint);
|
IND_LOCAL_OFF, theme.textFaint);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_FIND, theme.borderFocused);
|
||||||
|
|
||||||
// Lexer colors
|
// Lexer colors
|
||||||
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
m_lexer->setColor(theme.syntaxKeyword, QsciLexerCPP::Keyword);
|
||||||
@@ -832,11 +886,17 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find bar
|
// Find bar
|
||||||
if (m_findBar) {
|
if (m_findBarContainer) {
|
||||||
m_findBar->setStyleSheet(
|
m_findBar->setStyleSheet(
|
||||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||||
" padding: 4px 8px; font-size: 13px; }")
|
" padding: 4px 8px; font-size: 13px; }")
|
||||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
.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_prevHoveredNodeId = 0;
|
||||||
m_prevHoveredLine = -1;
|
m_prevHoveredLine = -1;
|
||||||
applyHoverHighlight();
|
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) {
|
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
|
||||||
@@ -1300,13 +1381,23 @@ int RcxEditor::currentNodeIndex() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::showFindBar() {
|
void RcxEditor::showFindBar() {
|
||||||
m_findBar->setVisible(true);
|
m_findBarContainer->setVisible(true);
|
||||||
m_findBar->setFocus();
|
m_findBar->setFocus();
|
||||||
m_findBar->selectAll();
|
m_findBar->selectAll();
|
||||||
|
m_findPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RcxEditor::dismissHistoryPopup() {
|
||||||
|
if (m_historyPopup)
|
||||||
|
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::hideFindBar() {
|
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();
|
m_sci->setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1385,7 +1476,12 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
|||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int nameW = lm.effectiveNameW;
|
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)
|
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||||
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||||
@@ -2156,6 +2252,12 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) {
|
|||||||
case Qt::Key_Return:
|
case Qt::Key_Return:
|
||||||
case Qt::Key_Enter:
|
case Qt::Key_Enter:
|
||||||
return beginInlineEdit(EditTarget::Value);
|
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: {
|
case Qt::Key_Tab: {
|
||||||
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
EditTarget order[] = {EditTarget::Name, EditTarget::Type, EditTarget::Value,
|
||||||
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
EditTarget::ArrayElementType, EditTarget::ArrayElementCount,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public:
|
|||||||
const LineMeta* metaForLine(int line) const;
|
const LineMeta* metaForLine(int line) const;
|
||||||
int currentNodeIndex() const;
|
int currentNodeIndex() const;
|
||||||
void scrollToNodeId(uint64_t nodeId);
|
void scrollToNodeId(uint64_t nodeId);
|
||||||
|
void showFindBar();
|
||||||
|
void dismissHistoryPopup();
|
||||||
|
|
||||||
// ── Column span computation ──
|
// ── Column span computation ──
|
||||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||||
@@ -79,6 +81,7 @@ signals:
|
|||||||
void inlineEditCancelled();
|
void inlineEditCancelled();
|
||||||
void typeSelectorRequested();
|
void typeSelectorRequested();
|
||||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||||
|
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||||
@@ -156,8 +159,9 @@ private:
|
|||||||
const NodeTree* m_disasmTree = nullptr;
|
const NodeTree* m_disasmTree = nullptr;
|
||||||
|
|
||||||
// ── Find bar ──
|
// ── Find bar ──
|
||||||
|
QWidget* m_findBarContainer = nullptr;
|
||||||
QLineEdit* m_findBar = nullptr;
|
QLineEdit* m_findBar = nullptr;
|
||||||
void showFindBar();
|
long m_findPos = 0;
|
||||||
void hideFindBar();
|
void hideFindBar();
|
||||||
|
|
||||||
// ── Reentrancy guards ──
|
// ── Reentrancy guards ──
|
||||||
|
|||||||
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
|
||||||
157
src/main.cpp
157
src/main.cpp
@@ -53,7 +53,6 @@
|
|||||||
#include "themes/thememanager.h"
|
#include "themes/thememanager.h"
|
||||||
#include "themes/themeeditor.h"
|
#include "themes/themeeditor.h"
|
||||||
#include "optionsdialog.h"
|
#include "optionsdialog.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
@@ -351,7 +350,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
|||||||
pal.setColor(QPalette::Text, theme.text);
|
pal.setColor(QPalette::Text, theme.text);
|
||||||
pal.setColor(QPalette::Button, theme.button);
|
pal.setColor(QPalette::Button, theme.button);
|
||||||
pal.setColor(QPalette::ButtonText, theme.text);
|
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::HighlightedText, theme.text);
|
||||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||||
@@ -389,13 +388,18 @@ public:
|
|||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
void applyMacTitleBarTheme(QWidget* window, const Theme& theme);
|
||||||
|
#endif
|
||||||
|
|
||||||
// MainWindow class declaration is in mainwindow.h
|
// MainWindow class declaration is in mainwindow.h
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||||
setWindowTitle("Reclass");
|
setWindowTitle("Reclass");
|
||||||
resize(1200, 800);
|
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
|
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint
|
||||||
| Qt::WindowMinMaxButtonsHint);
|
| Qt::WindowMinMaxButtonsHint);
|
||||||
|
|
||||||
@@ -403,6 +407,14 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
m_titleBar = new TitleBarWidget(this);
|
m_titleBar = new TitleBarWidget(this);
|
||||||
m_titleBar->applyTheme(ThemeManager::instance().current());
|
m_titleBar->applyTheme(ThemeManager::instance().current());
|
||||||
setMenuWidget(m_titleBar);
|
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
|
#ifdef _WIN32
|
||||||
// 1px top margin preserves DWM drop shadow on the frameless window
|
// 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)
|
// Restore menu bar title case setting (after menus are created)
|
||||||
{
|
{
|
||||||
QSettings s("Reclass", "Reclass");
|
QSettings s("Reclass", "Reclass");
|
||||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", false).toBool());
|
m_menuBarTitleCase = s.value("menuBarTitleCase", false).toBool();
|
||||||
if (s.value("showIcon", false).toBool())
|
applyMenuBarTitleCase(m_menuBarTitleCase);
|
||||||
|
if (m_titleBar && s.value("showIcon", false).toBool())
|
||||||
m_titleBar->setShowIcon(true);
|
m_titleBar->setShowIcon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,9 +520,42 @@ inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequ
|
|||||||
return result;
|
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() {
|
void MainWindow::createMenus() {
|
||||||
// File
|
// 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 &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass);
|
||||||
Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct);
|
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, "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);
|
Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||||
// Examples submenu — scan once at init
|
// Examples submenu — scan once at init
|
||||||
{
|
{
|
||||||
|
#ifdef __APPLE__
|
||||||
|
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
|
||||||
|
#else
|
||||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||||
|
#endif
|
||||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||||
if (!rcxFiles.isEmpty()) {
|
if (!rcxFiles.isEmpty()) {
|
||||||
auto* examples = file->addMenu("E&xamples");
|
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);
|
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
||||||
|
|
||||||
// Edit
|
// 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, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
||||||
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
||||||
|
|
||||||
// View
|
// 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, "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 = Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||||
m_removeSplitAction->setVisible(false);
|
m_removeSplitAction->setVisible(false);
|
||||||
@@ -626,7 +676,7 @@ void MainWindow::createMenus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tools
|
// Tools
|
||||||
auto* tools = m_titleBar->menuBar()->addMenu("&Tools");
|
auto* tools = m_menuBar->addMenu("&Tools");
|
||||||
Qt5Qt6AddAction(tools, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
Qt5Qt6AddAction(tools, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||||
tools->addSeparator();
|
tools->addSeparator();
|
||||||
const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
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);
|
Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
auto* plugins = m_menuBar->addMenu("&Plugins");
|
||||||
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
||||||
|
|
||||||
// Help
|
// 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);
|
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);
|
setupRenderedSci(pane.rendered);
|
||||||
rvLayout->addWidget(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 = new QLineEdit;
|
||||||
pane.findBar->setPlaceholderText("Find...");
|
pane.findBar->setPlaceholderText("Find...");
|
||||||
pane.findBar->setVisible(false);
|
|
||||||
const auto& fbTheme = ThemeManager::instance().current();
|
|
||||||
pane.findBar->setStyleSheet(
|
pane.findBar->setStyleSheet(
|
||||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||||
" padding: 4px 8px; font-size: 13px; }")
|
" padding: 4px 8px; font-size: 13px; }")
|
||||||
.arg(fbTheme.backgroundAlt.name())
|
.arg(fbTheme.backgroundAlt.name(), fbTheme.text.name(), fbTheme.border.name()));
|
||||||
.arg(fbTheme.text.name())
|
fcLayout->addWidget(ccPrevBtn);
|
||||||
.arg(fbTheme.border.name()));
|
fcLayout->addWidget(ccNextBtn);
|
||||||
rvLayout->addWidget(pane.findBar);
|
fcLayout->addWidget(ccCloseBtn);
|
||||||
|
fcLayout->addWidget(pane.findBar);
|
||||||
|
pane.findContainer->setVisible(false);
|
||||||
|
rvLayout->addWidget(pane.findContainer);
|
||||||
|
|
||||||
// Ctrl+F to show find bar
|
// Ctrl+F to show find bar
|
||||||
QsciScintilla* sci = pane.rendered;
|
QsciScintilla* sci = pane.rendered;
|
||||||
QLineEdit* fb = pane.findBar;
|
QLineEdit* fb = pane.findBar;
|
||||||
|
QWidget* fc = pane.findContainer;
|
||||||
auto* findAction = new QAction(pane.renderedContainer);
|
auto* findAction = new QAction(pane.renderedContainer);
|
||||||
findAction->setShortcut(QKeySequence::Find);
|
findAction->setShortcut(QKeySequence::Find);
|
||||||
findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||||
pane.renderedContainer->addAction(findAction);
|
pane.renderedContainer->addAction(findAction);
|
||||||
connect(findAction, &QAction::triggered, fb, [fb, sci]() {
|
connect(findAction, &QAction::triggered, fb, [fb, fc]() {
|
||||||
fb->setVisible(true);
|
fc->setVisible(true);
|
||||||
fb->setFocus();
|
fb->setFocus();
|
||||||
fb->selectAll();
|
fb->selectAll();
|
||||||
});
|
});
|
||||||
@@ -1103,8 +1178,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
|||||||
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
||||||
escAction->setShortcutContext(Qt::WidgetShortcut);
|
escAction->setShortcutContext(Qt::WidgetShortcut);
|
||||||
fb->addAction(escAction);
|
fb->addAction(escAction);
|
||||||
connect(escAction, &QAction::triggered, fb, [fb, sci]() {
|
connect(escAction, &QAction::triggered, fb, [fc, sci]() {
|
||||||
fb->setVisible(false);
|
fc->setVisible(false);
|
||||||
sci->setFocus();
|
sci->setFocus();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1119,6 +1194,21 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
|||||||
if (!sci->findNext())
|
if (!sci->findNext())
|
||||||
sci->findFirst(text, false, false, false, true, true, 0, 0);
|
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
|
pane.tabWidget->addTab(pane.renderedContainer, "C/C++"); // index 1
|
||||||
|
|
||||||
@@ -1671,9 +1761,14 @@ void MainWindow::toggleMcp() {
|
|||||||
void MainWindow::applyTheme(const Theme& theme) {
|
void MainWindow::applyTheme(const Theme& theme) {
|
||||||
applyGlobalTheme(theme);
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
applyMacTitleBarTheme(this, theme);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Dock separator is 1px via PM_DockWidgetSeparatorExtent in MenuBarStyle
|
// Dock separator is 1px via PM_DockWidgetSeparatorExtent in MenuBarStyle
|
||||||
|
|
||||||
// Custom title bar
|
// Custom title bar
|
||||||
|
if (m_titleBar)
|
||||||
m_titleBar->applyTheme(theme);
|
m_titleBar->applyTheme(theme);
|
||||||
|
|
||||||
// Update border overlay color
|
// Update border overlay color
|
||||||
@@ -1831,8 +1926,10 @@ void MainWindow::showOptionsDialog() {
|
|||||||
OptionsResult current;
|
OptionsResult current;
|
||||||
current.themeIndex = tm.currentIndex();
|
current.themeIndex = tm.currentIndex();
|
||||||
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
current.menuBarTitleCase = m_menuBarTitleCase;
|
||||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
current.showIcon = m_titleBar
|
||||||
|
? QSettings("Reclass", "Reclass").value("showIcon", false).toBool()
|
||||||
|
: false;
|
||||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
@@ -1850,11 +1947,12 @@ void MainWindow::showOptionsDialog() {
|
|||||||
setEditorFont(r.fontName);
|
setEditorFont(r.fontName);
|
||||||
|
|
||||||
if (r.menuBarTitleCase != current.menuBarTitleCase) {
|
if (r.menuBarTitleCase != current.menuBarTitleCase) {
|
||||||
m_titleBar->setMenuBarTitleCase(r.menuBarTitleCase);
|
applyMenuBarTitleCase(r.menuBarTitleCase);
|
||||||
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.showIcon != current.showIcon) {
|
if (r.showIcon != current.showIcon) {
|
||||||
|
if (m_titleBar)
|
||||||
m_titleBar->setShowIcon(r.showIcon);
|
m_titleBar->setShowIcon(r.showIcon);
|
||||||
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
||||||
}
|
}
|
||||||
@@ -1932,6 +2030,9 @@ MainWindow::TabState* MainWindow::tabByIndex(int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateWindowTitle() {
|
void MainWindow::updateWindowTitle() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
setWindowTitle(QStringLiteral("Reclass"));
|
||||||
|
#else
|
||||||
QString title;
|
QString title;
|
||||||
auto* sub = m_mdiArea->activeSubWindow();
|
auto* sub = m_mdiArea->activeSubWindow();
|
||||||
if (sub && m_tabs.contains(sub)) {
|
if (sub && m_tabs.contains(sub)) {
|
||||||
@@ -1945,6 +2046,7 @@ void MainWindow::updateWindowTitle() {
|
|||||||
title = "Reclass";
|
title = "Reclass";
|
||||||
}
|
}
|
||||||
setWindowTitle(title);
|
setWindowTitle(title);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Rendered view setup ──
|
// ── Rendered view setup ──
|
||||||
@@ -3110,7 +3212,7 @@ void MainWindow::changeEvent(QEvent* event) {
|
|||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
updateBorderColor(isActiveWindow() ? t.borderFocused : t.border);
|
updateBorderColor(isActiveWindow() ? t.borderFocused : t.border);
|
||||||
}
|
}
|
||||||
if (event->type() == QEvent::WindowStateChange)
|
if (event->type() == QEvent::WindowStateChange && m_titleBar)
|
||||||
m_titleBar->updateMaximizeIcon();
|
m_titleBar->updateMaximizeIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3140,6 +3242,9 @@ int main(int argc, char* argv[]) {
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
SetUnhandledExceptionFilter(crashHandler);
|
SetUnhandledExceptionFilter(crashHandler);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
|
||||||
|
#endif
|
||||||
|
|
||||||
DarkApp app(argc, argv);
|
DarkApp app(argc, argv);
|
||||||
app.setApplicationName("Reclass");
|
app.setApplicationName("Reclass");
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ private:
|
|||||||
QPushButton* m_btnReclass = nullptr;
|
QPushButton* m_btnReclass = nullptr;
|
||||||
QPushButton* m_btnRendered = nullptr;
|
QPushButton* m_btnRendered = nullptr;
|
||||||
TitleBarWidget* m_titleBar = nullptr;
|
TitleBarWidget* m_titleBar = nullptr;
|
||||||
|
QMenuBar* m_menuBar = nullptr;
|
||||||
|
bool m_menuBarTitleCase = false;
|
||||||
QWidget* m_borderOverlay = nullptr;
|
QWidget* m_borderOverlay = nullptr;
|
||||||
PluginManager m_pluginManager;
|
PluginManager m_pluginManager;
|
||||||
McpBridge* m_mcp = nullptr;
|
McpBridge* m_mcp = nullptr;
|
||||||
@@ -101,6 +103,7 @@ private:
|
|||||||
RcxEditor* editor = nullptr;
|
RcxEditor* editor = nullptr;
|
||||||
QsciScintilla* rendered = nullptr;
|
QsciScintilla* rendered = nullptr;
|
||||||
QLineEdit* findBar = nullptr;
|
QLineEdit* findBar = nullptr;
|
||||||
|
QWidget* findContainer = nullptr;
|
||||||
QWidget* renderedContainer = nullptr;
|
QWidget* renderedContainer = nullptr;
|
||||||
ViewMode viewMode = VM_Reclass;
|
ViewMode viewMode = VM_Reclass;
|
||||||
uint64_t lastRenderedRootId = 0;
|
uint64_t lastRenderedRootId = 0;
|
||||||
@@ -118,6 +121,7 @@ private:
|
|||||||
void rebuildAllDocs();
|
void rebuildAllDocs();
|
||||||
|
|
||||||
void createMenus();
|
void createMenus();
|
||||||
|
void applyMenuBarTitleCase(bool titleCase);
|
||||||
void createStatusBar();
|
void createStatusBar();
|
||||||
void showPluginsDialog();
|
void showPluginsDialog();
|
||||||
void populateSourceMenu();
|
void populateSourceMenu();
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ ThemeManager::ThemeManager() {
|
|||||||
// ── Load built-in themes from JSON files next to the executable ──
|
// ── Load built-in themes from JSON files next to the executable ──
|
||||||
|
|
||||||
QString ThemeManager::builtInDir() const {
|
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";
|
return QCoreApplication::applicationDirPath() + "/themes";
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeManager::loadBuiltInThemes() {
|
void ThemeManager::loadBuiltInThemes() {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
|||||||
// App label
|
// App label
|
||||||
m_appLabel->setStyleSheet(
|
m_appLabel->setStyleSheet(
|
||||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
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.
|
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
// 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();
|
QPalette mbPal = m_menuBar->palette();
|
||||||
mbPal.setColor(QPalette::Window, theme.background);
|
mbPal.setColor(QPalette::Window, theme.background);
|
||||||
mbPal.setColor(QPalette::Button, 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->setPalette(mbPal);
|
||||||
m_menuBar->setAutoFillBackground(false);
|
m_menuBar->setAutoFillBackground(false);
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ void TitleBarWidget::setShowIcon(bool show) {
|
|||||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||||
m_appLabel->setStyleSheet(
|
m_appLabel->setStyleSheet(
|
||||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||||
.arg(m_theme.textDim.name()));
|
.arg(m_theme.text.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -815,6 +815,68 @@ private slots:
|
|||||||
QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
|
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() {
|
void testStaticFieldTypeChangePreservesFlags() {
|
||||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user