mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add macOS support and CI
This commit is contained in:
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- "**"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -22,9 +22,9 @@ jobs:
|
||||
- name: Install Qt6 and MinGW
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_mingw'
|
||||
tools: 'tools_mingw1310,qt.tools.win64_mingw1310'
|
||||
version: "6.8.1"
|
||||
arch: "win64_mingw"
|
||||
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||
cache: true
|
||||
|
||||
- name: Configure
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
version: "6.8.1"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -141,9 +141,66 @@ jobs:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
qt_arch: clang_arm64
|
||||
artifact_name: Reclass-macos-arm64-qt6
|
||||
zip_name: Reclass-macos-arm64-qt6.zip
|
||||
- os: macos-15-intel
|
||||
qt_arch: clang_64
|
||||
artifact_name: Reclass-macos-x86_64-qt6
|
||||
zip_name: Reclass-macos-x86_64-qt6.zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake ninja qt
|
||||
|
||||
- name: Configure Qt paths
|
||||
run: |
|
||||
QT_PREFIX="$(brew --prefix qt)"
|
||||
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
|
||||
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package app zip
|
||||
run: |
|
||||
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
|
||||
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
|
||||
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
|
||||
fi
|
||||
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
|
||||
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
|
||||
codesign --force --deep --sign - build/Reclass.app
|
||||
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.zip_name }}
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux]
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -168,5 +225,7 @@ jobs:
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
|
||||
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
*.DS_Store
|
||||
|
||||
@@ -113,6 +113,8 @@ add_executable(Reclass
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/macos_titlebar.h
|
||||
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
@@ -124,6 +126,16 @@ add_executable(Reclass
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(Reclass PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_ICON_FILE "class.icns"
|
||||
)
|
||||
target_sources(Reclass PRIVATE src/icons/class.icns)
|
||||
set_source_files_properties(src/icons/class.icns
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
endif()
|
||||
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
@@ -150,6 +162,12 @@ foreach(_tf ${_theme_files})
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_theme_files})
|
||||
set_source_files_properties(${_theme_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
@@ -158,6 +176,12 @@ foreach(_ef ${_example_files})
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_example_files})
|
||||
set_source_files_properties(${_example_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||
endif()
|
||||
|
||||
include(deploy)
|
||||
|
||||
|
||||
@@ -481,8 +505,10 @@ if(BUILD_TESTING)
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
|
||||
14
README.md
14
README.md
@@ -49,7 +49,7 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
|
||||
- [ ] Safe mode
|
||||
- [ ] File import for other Reclass instances
|
||||
- [ ] Expose UI functionality to plugins
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] iOS support
|
||||
- [ ] Display RTTI information
|
||||
|
||||
## Data Sources
|
||||
@@ -92,7 +92,7 @@ Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `
|
||||
|
||||
### Quick Build
|
||||
|
||||
```bash
|
||||
```/dev/null/commands.sh#L1-4
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
@@ -101,6 +101,16 @@ cd Reclass
|
||||
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
### macOS Build
|
||||
|
||||
```/dev/null/commands.sh#L1-2
|
||||
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
|
||||
```
|
||||
|
||||
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
|
||||
|
||||
Note: macOS Gatekeeper may block unsigned apps. If the app won’t open, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
|
||||
### Manual Build (MinGW)
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
|
||||
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
|
||||
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
|
||||
90
src/main.cpp
90
src/main.cpp
@@ -53,7 +53,6 @@
|
||||
#include "themes/thememanager.h"
|
||||
#include "themes/themeeditor.h"
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
@@ -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,9 +1721,14 @@ 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
|
||||
if (m_titleBar)
|
||||
m_titleBar->applyTheme(theme);
|
||||
|
||||
// Update border overlay color
|
||||
@@ -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,11 +1907,12 @@ 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -33,7 +33,12 @@ ThemeManager::ThemeManager() {
|
||||
// ── Load built-in themes from JSON files next to the executable ──
|
||||
|
||||
QString ThemeManager::builtInDir() const {
|
||||
#ifdef Q_OS_MACOS
|
||||
// In a macOS .app bundle, resources live in Contents/Resources, not Contents/MacOS
|
||||
return QCoreApplication::applicationDirPath() + "/../Resources/themes";
|
||||
#else
|
||||
return QCoreApplication::applicationDirPath() + "/themes";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ThemeManager::loadBuiltInThemes() {
|
||||
|
||||
@@ -74,7 +74,7 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
// App label
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
.arg(theme.text.name()));
|
||||
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
@@ -82,7 +82,7 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.text);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
m_appLabel->setText(QStringLiteral("Reclass"));
|
||||
m_appLabel->setStyleSheet(
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(m_theme.textDim.name()));
|
||||
.arg(m_theme.text.name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user