diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0e0420..651651b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 742d70a..e4d62b2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ CMakeUserPresets.json plugins/RcNetPluginCompatLayer/bridge/obj plugins/RcNetPluginCompatLayer/bridge/bin .cache +*.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 073969d..6bac0ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,8 @@ add_executable(Reclass src/optionsdialog.cpp src/titlebar.h src/titlebar.cpp + src/macos_titlebar.h + $<$:src/macos_titlebar.mm> src/mcp/mcp_bridge.h src/mcp/mcp_bridge.cpp src/addressparser.h @@ -124,6 +126,16 @@ add_executable(Reclass $<$: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 $ --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) diff --git a/README.md b/README.md index 1b589c4..6fc8ca8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor - [ ] Safe mode - [ ] File import for other Reclass instances - [ ] Expose UI functionality to plugins -- [ ] iOS/macOS support +- [ ] iOS support - [ ] Display RTTI information ## Data Sources @@ -92,7 +92,7 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via ` ### Quick Build -```bash +```/dev/null/commands.sh#L1-4 git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git cd Reclass .\scripts\build_qscintilla.ps1 @@ -101,6 +101,16 @@ cd Reclass The build script auto-detects your Qt install location. +### macOS Build + +```/dev/null/commands.sh#L1-2 +./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package +``` + +If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`. + +Note: macOS Gatekeeper may block unsigned apps. If the app won’t open, go to **System Settings → Privacy & Security** and click **Open Anyway**. + ### Manual Build (MinGW) 1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning) diff --git a/scripts/build_macos.sh b/scripts/build_macos.sh new file mode 100755 index 0000000..b9484a3 --- /dev/null +++ b/scripts/build_macos.sh @@ -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 Qt installation prefix (e.g. /opt/homebrew/opt/qt) + --build-type Release | Debug | RelWithDebInfo | MinSizeRel (default: Release) + --build-dir Build directory (default: /build) + --generator 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 diff --git a/src/icons/class.icns b/src/icons/class.icns new file mode 100644 index 0000000..4887d91 Binary files /dev/null and b/src/icons/class.icns differ diff --git a/src/macos_titlebar.h b/src/macos_titlebar.h new file mode 100644 index 0000000..65e19f1 --- /dev/null +++ b/src/macos_titlebar.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +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 \ No newline at end of file diff --git a/src/macos_titlebar.mm b/src/macos_titlebar.mm new file mode 100644 index 0000000..7c1cb8c --- /dev/null +++ b/src/macos_titlebar.mm @@ -0,0 +1,43 @@ +#include "macos_titlebar.h" +#include "themes/theme.h" + +#import +#include +#include + +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(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 diff --git a/src/main.cpp b/src/main.cpp index 0dfcc1d..565070e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -53,7 +53,6 @@ #include "themes/thememanager.h" #include "themes/themeeditor.h" #include "optionsdialog.h" - #ifdef _WIN32 #include #include @@ -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); } @@ -1671,10 +1721,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 +1886,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 +1907,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 +1990,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 +2006,7 @@ void MainWindow::updateWindowTitle() { title = "Reclass"; } setWindowTitle(title); +#endif } // ── Rendered view setup ── @@ -3110,7 +3172,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(); } diff --git a/src/mainwindow.h b/src/mainwindow.h index aaf31f0..d40566c 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -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; @@ -118,6 +120,7 @@ private: void rebuildAllDocs(); void createMenus(); + void applyMenuBarTitleCase(bool titleCase); void createStatusBar(); void showPluginsDialog(); void populateSourceMenu(); diff --git a/src/themes/thememanager.cpp b/src/themes/thememanager.cpp index e0cb41f..aeb2f05 100644 --- a/src/themes/thememanager.cpp +++ b/src/themes/thememanager.cpp @@ -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() { diff --git a/src/titlebar.cpp b/src/titlebar.cpp index fdba351..3fe4dfb 100644 --- a/src/titlebar.cpp +++ b/src/titlebar.cpp @@ -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())); } }