mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
23 Commits
latest-lin
...
snapshot-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ff4dfe71 | ||
|
|
735e4ea9f7 | ||
|
|
d937d2f42e | ||
|
|
3685530287 | ||
|
|
9e90f66ca0 | ||
|
|
f53fa84a15 | ||
|
|
13e28e8791 | ||
|
|
079b3121ce | ||
|
|
5e40349768 | ||
|
|
8dd6110ec6 | ||
|
|
eb27fc7988 | ||
|
|
85994d68b9 | ||
|
|
55dc5d5875 | ||
|
|
3a92336132 | ||
|
|
f9b33f2ba7 | ||
|
|
f2dab07870 | ||
|
|
9d22a5ed69 | ||
|
|
193ab81ecf | ||
|
|
aa0840b332 | ||
|
|
f3631f17ff | ||
|
|
42e9bde7ba | ||
|
|
07fedf0ae8 | ||
|
|
2e02a01495 |
180
.github/workflows/build.yml
vendored
180
.github/workflows/build.yml
vendored
@@ -43,11 +43,11 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: Reclass-Windows-x64
|
name: Reclass-win64-qt6
|
||||||
path: |
|
path: |
|
||||||
build/Reclass.exe
|
build/Reclass.exe
|
||||||
build/ReclassMcpBridge.exe
|
build/ReclassMcpBridge.exe
|
||||||
build/Plugins/
|
build/Plugins/*.dll
|
||||||
build/*.dll
|
build/*.dll
|
||||||
build/platforms/
|
build/platforms/
|
||||||
build/styles/
|
build/styles/
|
||||||
@@ -56,6 +56,12 @@ jobs:
|
|||||||
build/themes/
|
build/themes/
|
||||||
build/screenshot.png
|
build/screenshot.png
|
||||||
|
|
||||||
|
- name: Get date tag
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
id: date
|
||||||
|
shell: bash
|
||||||
|
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Package release zip
|
- name: Package release zip
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -68,28 +74,29 @@ jobs:
|
|||||||
cp -r build/styles release/ 2>/dev/null || true
|
cp -r build/styles release/ 2>/dev/null || true
|
||||||
cp -r build/imageformats release/ 2>/dev/null || true
|
cp -r build/imageformats release/ 2>/dev/null || true
|
||||||
cp -r build/iconengines release/ 2>/dev/null || true
|
cp -r build/iconengines release/ 2>/dev/null || true
|
||||||
cp -r build/Plugins release/ 2>/dev/null || true
|
mkdir -p release/Plugins
|
||||||
|
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||||
cp -r build/themes release/ 2>/dev/null || true
|
cp -r build/themes release/ 2>/dev/null || true
|
||||||
cp build/screenshot.png release/ 2>/dev/null || true
|
cp build/screenshot.png release/ 2>/dev/null || true
|
||||||
cd release && 7z a ../win64-reclass-latest.zip *
|
cd release && 7z a ../Reclass-win64-qt6.zip *
|
||||||
|
|
||||||
- name: Update win64 release
|
- name: Upload release asset
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: latest-win64
|
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||||
name: win64
|
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||||
body: |
|
body: |
|
||||||
Windows x64 build from main branch.
|
Automated snapshot from main branch.
|
||||||
Commit: ${{ github.sha }}
|
Commit: ${{ github.sha }}
|
||||||
prerelease: true
|
prerelease: false
|
||||||
files: win64-reclass-latest.zip
|
files: Reclass-win64-qt6.zip
|
||||||
make_latest: false
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
runs-on: ubuntu-latest
|
needs: windows
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -106,7 +113,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ninja-build libgl1-mesa-dev
|
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
@@ -119,35 +126,146 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
QT_QPA_PLATFORM: offscreen
|
QT_QPA_PLATFORM: offscreen
|
||||||
|
|
||||||
|
- name: Create AppImage
|
||||||
|
run: |
|
||||||
|
# Download linuxdeploy and Qt plugin
|
||||||
|
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||||
|
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||||
|
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
|
||||||
|
|
||||||
|
# Build AppDir structure
|
||||||
|
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
|
||||||
|
cp build/Reclass AppDir/usr/bin/
|
||||||
|
cp build/ReclassMcpBridge AppDir/usr/bin/
|
||||||
|
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
|
||||||
|
mkdir -p AppDir/usr/bin/Plugins
|
||||||
|
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
|
||||||
|
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
|
||||||
|
|
||||||
|
# Create AppImage with Qt libs bundled
|
||||||
|
# install-qt-action adds Qt bin to PATH; find qmake there
|
||||||
|
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
|
||||||
|
echo "Found qmake at: $QMAKE_BIN"
|
||||||
|
export QMAKE="$QMAKE_BIN"
|
||||||
|
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
|
||||||
|
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
|
||||||
|
export EXTRA_QT_PLUGINS="svg;iconengines"
|
||||||
|
./linuxdeploy-x86_64.AppImage --appdir AppDir \
|
||||||
|
--desktop-file deploy/Reclass.desktop \
|
||||||
|
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
|
||||||
|
--plugin qt \
|
||||||
|
--output appimage
|
||||||
|
# Rename to final name
|
||||||
|
ls Reclass-*.AppImage
|
||||||
|
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: Reclass-Linux-x64
|
name: Reclass-linux64-qt6
|
||||||
path: |
|
path: Reclass-linux64-qt6.AppImage
|
||||||
build/Reclass
|
|
||||||
build/ReclassMcpBridge
|
|
||||||
|
|
||||||
- name: Package release tarball
|
- name: Get date tag
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: |
|
id: date
|
||||||
mkdir -p release
|
shell: bash
|
||||||
cp build/Reclass release/
|
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||||
cp build/ReclassMcpBridge release/
|
|
||||||
cp -r build/themes release/ 2>/dev/null || true
|
|
||||||
tar czf linux64-reclass-latest.tar.gz -C release .
|
|
||||||
|
|
||||||
- name: Update linux64 release
|
- name: Upload release asset
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: latest-linux64
|
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||||
name: linux64
|
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||||
body: |
|
body: |
|
||||||
Linux x64 build from main branch.
|
Automated snapshot from main branch.
|
||||||
Commit: ${{ github.sha }}
|
Commit: ${{ github.sha }}
|
||||||
prerelease: true
|
prerelease: false
|
||||||
files: linux64-reclass-latest.tar.gz
|
files: Reclass-linux64-qt6.AppImage
|
||||||
make_latest: false
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
windows-qt5:
|
||||||
|
needs: linux
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Qt5
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
version: '5.15.2'
|
||||||
|
arch: 'win64_msvc2019_64'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
with:
|
||||||
|
arch: x64
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_windbg_provider|test_com_security|test_format|test_command_row|test_type_selector"
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Reclass-win64-qt5
|
||||||
|
path: |
|
||||||
|
build/Reclass.exe
|
||||||
|
build/ReclassMcpBridge.exe
|
||||||
|
build/Plugins/*.dll
|
||||||
|
build/*.dll
|
||||||
|
build/platforms/
|
||||||
|
build/styles/
|
||||||
|
build/imageformats/
|
||||||
|
build/iconengines/
|
||||||
|
build/themes/
|
||||||
|
build/screenshot.png
|
||||||
|
|
||||||
|
- name: Get date tag
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
id: date
|
||||||
|
shell: bash
|
||||||
|
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Package release zip
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p release
|
||||||
|
cp build/Reclass.exe release/
|
||||||
|
cp build/ReclassMcpBridge.exe release/
|
||||||
|
cp build/*.dll release/ 2>/dev/null || true
|
||||||
|
cp -r build/platforms release/ 2>/dev/null || true
|
||||||
|
cp -r build/styles release/ 2>/dev/null || true
|
||||||
|
cp -r build/imageformats release/ 2>/dev/null || true
|
||||||
|
cp -r build/iconengines release/ 2>/dev/null || true
|
||||||
|
mkdir -p release/Plugins
|
||||||
|
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||||
|
cp -r build/themes release/ 2>/dev/null || true
|
||||||
|
cp build/screenshot.png release/ 2>/dev/null || true
|
||||||
|
cd release && 7z a ../Reclass-win64-qt5.zip *
|
||||||
|
|
||||||
|
- name: Upload release asset
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||||
|
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||||
|
body: |
|
||||||
|
Automated snapshot from main branch.
|
||||||
|
Commit: ${{ github.sha }}
|
||||||
|
prerelease: false
|
||||||
|
files: Reclass-win64-qt5.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -261,6 +261,12 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||||
add_test(NAME test_theme COMMAND test_theme)
|
add_test(NAME test_theme COMMAND test_theme)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||||
@@ -291,7 +297,7 @@ if(BUILD_TESTING)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
if(WIN32)
|
|
||||||
add_subdirectory(plugins/ProcessMemory)
|
add_subdirectory(plugins/ProcessMemory)
|
||||||
|
if(WIN32)
|
||||||
add_subdirectory(plugins/WinDbgMemory)
|
add_subdirectory(plugins/WinDbgMemory)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ This tool helps you inspect raw bytes and interpret them as types (structs, arra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- Plugin system is partially implemented. Some UI bugs exist.
|
|
||||||
- Vector/Matrix improvements have been made but are not entirely complete.
|
|
||||||
- Every edit goes through a full undo/redo system.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
1. Prerequisites
|
1. Prerequisites
|
||||||
|
|||||||
8
deploy/Reclass.desktop
Normal file
8
deploy/Reclass.desktop
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Reclass
|
||||||
|
Comment=Memory structure reverse engineering tool
|
||||||
|
Exec=Reclass
|
||||||
|
Icon=reclass
|
||||||
|
Categories=Development;Debugger;
|
||||||
|
Terminal=false
|
||||||
@@ -32,7 +32,7 @@ enum class NodeKind : uint8_t {
|
|||||||
|
|
||||||
} // namespace rcx (temporarily close for qHash)
|
} // namespace rcx (temporarily close for qHash)
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||||
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast<uint8_t>(key), seed); }
|
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
|
||||||
#endif
|
#endif
|
||||||
namespace rcx { // reopen
|
namespace rcx { // reopen
|
||||||
|
|
||||||
|
|||||||
@@ -1401,7 +1401,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
int margin0Width = (int)m_sci->SendScintilla(
|
int margin0Width = (int)m_sci->SendScintilla(
|
||||||
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
QsciScintillaBase::SCI_GETMARGINWIDTHN, 0UL, 0L);
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
if ((int)me->position().x() < margin0Width) {
|
if ((int)me->position().x() < margin0Width) {
|
||||||
|
#else
|
||||||
|
if ((int)me->pos().x() < margin0Width) {
|
||||||
|
#endif
|
||||||
m_relativeOffsets = !m_relativeOffsets;
|
m_relativeOffsets = !m_relativeOffsets;
|
||||||
reformatMargins();
|
reformatMargins();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
115
src/main.cpp
115
src/main.cpp
@@ -220,8 +220,8 @@ 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.hover);
|
pal.setColor(QPalette::Highlight, theme.selection);
|
||||||
pal.setColor(QPalette::HighlightedText, theme.indHoverSpan);
|
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);
|
||||||
pal.setColor(QPalette::Mid, theme.border);
|
pal.setColor(QPalette::Mid, theme.border);
|
||||||
@@ -313,8 +313,10 @@ 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)
|
||||||
{
|
{
|
||||||
bool titleCase = QSettings("Reclass", "Reclass").value("menuBarTitleCase", true).toBool();
|
QSettings s("Reclass", "Reclass");
|
||||||
m_titleBar->setMenuBarTitleCase(titleCase);
|
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool());
|
||||||
|
if (s.value("showIcon", false).toBool())
|
||||||
|
m_titleBar->setShowIcon(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
|
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
|
||||||
@@ -354,35 +356,48 @@ QIcon MainWindow::makeIcon(const QString& svgPath) {
|
|||||||
return QIcon(svgPath);
|
return QIcon(svgPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template < typename...Args >
|
||||||
|
inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequence &shortcut, const QIcon &icon, Args&&...args)
|
||||||
|
{
|
||||||
|
QAction *result = menu->addAction(icon, text);
|
||||||
|
if (!shortcut.isEmpty())
|
||||||
|
result->setShortcut(shortcut);
|
||||||
|
QObject::connect(result, &QAction::triggered, std::forward<Args>(args)...);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::createMenus() {
|
void MainWindow::createMenus() {
|
||||||
// File
|
// File
|
||||||
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
||||||
file->addAction("&New", QKeySequence::New, this, &MainWindow::newDocument);
|
Qt5Qt6AddAction(file, "&New", QKeySequence::New, QIcon(), this, &MainWindow::newDocument);
|
||||||
file->addAction("New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), this, &MainWindow::newFile);
|
Qt5Qt6AddAction(file, "New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newFile);
|
||||||
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", QKeySequence::Open, this, &MainWindow::openFile);
|
Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile);
|
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||||
file->addAction(makeIcon(":/vsicons/save-as.svg"), "Save &As...", QKeySequence::SaveAs, this, &MainWindow::saveFileAs);
|
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp);
|
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
m_mcpAction = file->addAction(QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server", this, &MainWindow::toggleMcp);
|
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
file->addAction(makeIcon(":/vsicons/settings-gear.svg"), "&Options...", this, &MainWindow::showOptionsDialog);
|
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||||
|
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", QKeySequence(Qt::Key_Close), this, &QMainWindow::close);
|
Qt5Qt6AddAction(file, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||||
|
file->addSeparator();
|
||||||
|
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_titleBar->menuBar()->addMenu("&Edit");
|
||||||
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", QKeySequence::Undo, this, &MainWindow::undo);
|
Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
||||||
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", QKeySequence::Redo, this, &MainWindow::redo);
|
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
||||||
edit->addSeparator();
|
edit->addSeparator();
|
||||||
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
Qt5Qt6AddAction(edit, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||||
|
|
||||||
// View
|
// View
|
||||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||||
view->addAction(makeIcon(":/vsicons/split-horizontal.svg"), "Split &Horizontal", this, &MainWindow::splitView);
|
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
||||||
view->addAction(makeIcon(":/vsicons/chrome-close.svg"), "&Unsplit", this, &MainWindow::unsplitView);
|
Qt5Qt6AddAction(view, "&Unsplit", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||||
view->addSeparator();
|
view->addSeparator();
|
||||||
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
||||||
auto* fontGroup = new QActionGroup(this);
|
auto* fontGroup = new QActionGroup(this);
|
||||||
@@ -417,35 +432,18 @@ void MainWindow::createMenus() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
themeMenu->addSeparator();
|
themeMenu->addSeparator();
|
||||||
themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme);
|
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
|
||||||
|
|
||||||
view->addSeparator();
|
view->addSeparator();
|
||||||
auto* actShowIcon = view->addAction("Show &Icon");
|
|
||||||
actShowIcon->setCheckable(true);
|
|
||||||
actShowIcon->setChecked(settings.value("showIcon", false).toBool());
|
|
||||||
if (actShowIcon->isChecked()) m_titleBar->setShowIcon(true);
|
|
||||||
connect(actShowIcon, &QAction::toggled, this, [this](bool checked) {
|
|
||||||
m_titleBar->setShowIcon(checked);
|
|
||||||
QSettings s("Reclass", "Reclass");
|
|
||||||
s.setValue("showIcon", checked);
|
|
||||||
});
|
|
||||||
view->addAction(m_workspaceDock->toggleViewAction());
|
view->addAction(m_workspaceDock->toggleViewAction());
|
||||||
|
|
||||||
// Node
|
|
||||||
auto* node = m_titleBar->menuBar()->addMenu("&Node");
|
|
||||||
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", QKeySequence(Qt::Key_Insert), this, &MainWindow::addNode);
|
|
||||||
node->addAction(makeIcon(":/vsicons/remove.svg"), "&Remove Field", QKeySequence::Delete, this, &MainWindow::removeNode);
|
|
||||||
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", QKeySequence(Qt::Key_T), this, &MainWindow::changeNodeType);
|
|
||||||
node->addAction(makeIcon(":/vsicons/edit.svg"), "Re&name", QKeySequence(Qt::Key_F2), this, &MainWindow::renameNodeAction);
|
|
||||||
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||||
plugins->addAction("&Manage Plugins...", 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_titleBar->menuBar()->addMenu("&Help");
|
||||||
help->addAction(makeIcon(":/vsicons/question.svg"), "&About Reclass", this, &MainWindow::about);
|
Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::createStatusBar() {
|
void MainWindow::createStatusBar() {
|
||||||
@@ -838,6 +836,10 @@ void MainWindow::saveFileAs() {
|
|||||||
project_save(nullptr, true);
|
project_save(nullptr, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::closeFile() {
|
||||||
|
project_close();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::addNode() {
|
void MainWindow::addNode() {
|
||||||
auto* ctrl = activeController();
|
auto* ctrl = activeController();
|
||||||
if (!ctrl) return;
|
if (!ctrl) return;
|
||||||
@@ -1042,6 +1044,7 @@ void MainWindow::showOptionsDialog() {
|
|||||||
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_titleBar->menuBarTitleCase();
|
||||||
|
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||||
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", false).toBool();
|
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
|
||||||
|
|
||||||
@@ -1061,6 +1064,11 @@ void MainWindow::showOptionsDialog() {
|
|||||||
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.showIcon != current.showIcon) {
|
||||||
|
m_titleBar->setShowIcon(r.showIcon);
|
||||||
|
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
||||||
|
}
|
||||||
|
|
||||||
if (r.safeMode != current.safeMode)
|
if (r.safeMode != current.safeMode)
|
||||||
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
|
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
|
||||||
|
|
||||||
@@ -1392,6 +1400,10 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
delete doc;
|
delete doc;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close all existing tabs so the project replaces the current state
|
||||||
|
m_mdiArea->closeAllSubWindows();
|
||||||
|
|
||||||
auto* sub = createTab(doc);
|
auto* sub = createTab(doc);
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
return sub;
|
return sub;
|
||||||
@@ -1437,11 +1449,36 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||||
m_workspaceTree->setMouseTracking(true);
|
m_workspaceTree->setMouseTracking(true);
|
||||||
|
|
||||||
|
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||||
|
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||||
|
if (!index.isValid()) return;
|
||||||
|
|
||||||
|
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||||
|
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||||
|
if (structId == 0 || structId == rcx::kGroupSentinel) return;
|
||||||
|
|
||||||
|
auto subVar = index.data(Qt::UserRole);
|
||||||
|
if (!subVar.isValid()) return;
|
||||||
|
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
|
||||||
|
if (!sub || !m_tabs.contains(sub)) return;
|
||||||
|
|
||||||
|
QMenu menu;
|
||||||
|
auto* deleteAction = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete");
|
||||||
|
if (menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)) == deleteAction) {
|
||||||
|
auto& tab = m_tabs[sub];
|
||||||
|
int ni = tab.doc->tree.indexOfId(structId);
|
||||||
|
if (ni >= 0) {
|
||||||
|
tab.ctrl->removeNode(ni);
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
m_workspaceDock->setWidget(m_workspaceTree);
|
m_workspaceDock->setWidget(m_workspaceTree);
|
||||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||||
m_workspaceDock->hide();
|
m_workspaceDock->hide();
|
||||||
|
|
||||||
|
|
||||||
connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) {
|
connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) {
|
||||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ private slots:
|
|||||||
void openFile();
|
void openFile();
|
||||||
void saveFile();
|
void saveFile();
|
||||||
void saveFileAs();
|
void saveFileAs();
|
||||||
|
void closeFile();
|
||||||
|
|
||||||
void addNode();
|
void addNode();
|
||||||
void removeNode();
|
void removeNode();
|
||||||
|
|||||||
@@ -793,7 +793,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.contains("pid")) {
|
if (args.contains("pid")) {
|
||||||
uint32_t pid = (uint32_t)args.value("pid").toInteger();
|
uint32_t pid = (uint32_t)args.value("pid").toInt();
|
||||||
QString name = args.value("processName").toString();
|
QString name = args.value("processName").toString();
|
||||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
||||||
QString target = QString("%1:%2").arg(pid).arg(name);
|
QString target = QString("%1:%2").arg(pid).arg(name);
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QTreeWidgetItem>
|
#include <QTreeWidgetItem>
|
||||||
#include <QGraphicsDropShadowEffect>
|
|
||||||
#include <QEvent>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -20,8 +18,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
setWindowTitle("Options");
|
setWindowTitle("Options");
|
||||||
setFixedSize(700, 450);
|
setFixedSize(700, 450);
|
||||||
|
|
||||||
const auto& t = ThemeManager::instance().current();
|
|
||||||
|
|
||||||
auto* mainLayout = new QVBoxLayout(this);
|
auto* mainLayout = new QVBoxLayout(this);
|
||||||
mainLayout->setSpacing(8);
|
mainLayout->setSpacing(8);
|
||||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||||
@@ -87,6 +83,10 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||||
visualLayout->addRow(m_titleCaseCheck);
|
visualLayout->addRow(m_titleCaseCheck);
|
||||||
|
|
||||||
|
m_showIconCheck = new QCheckBox("Show icon in title bar");
|
||||||
|
m_showIconCheck->setChecked(current.showIcon);
|
||||||
|
visualLayout->addRow(m_showIconCheck);
|
||||||
|
|
||||||
generalLayout->addWidget(visualGroup);
|
generalLayout->addWidget(visualGroup);
|
||||||
|
|
||||||
// Safe Mode group box
|
// Safe Mode group box
|
||||||
@@ -96,8 +96,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
|
|
||||||
m_safeModeCheck = new QCheckBox("Safe Mode");
|
m_safeModeCheck = new QCheckBox("Safe Mode");
|
||||||
m_safeModeCheck->setChecked(current.safeMode);
|
m_safeModeCheck->setChecked(current.safeMode);
|
||||||
m_safeModeCheck->setStyleSheet(QStringLiteral(
|
|
||||||
"QCheckBox { font-weight: bold; }"));
|
|
||||||
safeModeLayout->addWidget(m_safeModeCheck);
|
safeModeLayout->addWidget(m_safeModeCheck);
|
||||||
|
|
||||||
auto* safeModeDesc = new QLabel(
|
auto* safeModeDesc = new QLabel(
|
||||||
@@ -127,8 +125,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
|
|
||||||
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
|
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
|
||||||
m_autoMcpCheck->setChecked(current.autoStartMcp);
|
m_autoMcpCheck->setChecked(current.autoStartMcp);
|
||||||
m_autoMcpCheck->setStyleSheet(QStringLiteral(
|
|
||||||
"QCheckBox { font-weight: bold; }"));
|
|
||||||
mcpLayout->addWidget(m_autoMcpCheck);
|
mcpLayout->addWidget(m_autoMcpCheck);
|
||||||
|
|
||||||
auto* mcpDesc = new QLabel(
|
auto* mcpDesc = new QLabel(
|
||||||
@@ -144,6 +140,18 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
m_pages->addWidget(aiPage); // index 1
|
m_pages->addWidget(aiPage); // index 1
|
||||||
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
|
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
|
||||||
|
|
||||||
|
// -- Generator page --
|
||||||
|
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
||||||
|
|
||||||
|
auto* generatorPage = new QWidget;
|
||||||
|
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||||
|
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
generatorLayout->setSpacing(8);
|
||||||
|
generatorLayout->addStretch();
|
||||||
|
|
||||||
|
m_pages->addWidget(generatorPage); // index 2
|
||||||
|
m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage);
|
||||||
|
|
||||||
middleLayout->addWidget(m_pages, 1);
|
middleLayout->addWidget(m_pages, 1);
|
||||||
|
|
||||||
mainLayout->addLayout(middleLayout, 1);
|
mainLayout->addLayout(middleLayout, 1);
|
||||||
@@ -151,6 +159,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
// Tree <-> page connection
|
// Tree <-> page connection
|
||||||
m_itemPageIndex[generalItem] = 0;
|
m_itemPageIndex[generalItem] = 0;
|
||||||
m_itemPageIndex[aiItem] = 1;
|
m_itemPageIndex[aiItem] = 1;
|
||||||
|
m_itemPageIndex[generatorItem] = 2;
|
||||||
connect(m_tree, &QTreeWidget::currentItemChanged, this,
|
connect(m_tree, &QTreeWidget::currentItemChanged, this,
|
||||||
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
|
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
@@ -165,106 +174,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
mainLayout->addWidget(buttons);
|
mainLayout->addWidget(buttons);
|
||||||
|
|
||||||
// -- Styling --
|
|
||||||
|
|
||||||
// Combo boxes: set directly so the popup (top-level widget) inherits it
|
|
||||||
QString comboStyle = QStringLiteral(
|
|
||||||
"QComboBox {"
|
|
||||||
" background: %1; color: %2; border: 1px solid %3;"
|
|
||||||
" padding: 3px 8px; font-size: 12px;"
|
|
||||||
"}"
|
|
||||||
"QComboBox::drop-down {"
|
|
||||||
" border: none; border-left: 1px solid %3;"
|
|
||||||
" width: 20px;"
|
|
||||||
"}"
|
|
||||||
"QComboBox::down-arrow {"
|
|
||||||
" image: url(:/vsicons/chevron-down.svg);"
|
|
||||||
" width: 12px; height: 12px;"
|
|
||||||
"}"
|
|
||||||
"QComboBox QAbstractItemView {"
|
|
||||||
" background: %1; color: %2; border: 1px solid %3;"
|
|
||||||
" selection-background-color: %4;"
|
|
||||||
"}")
|
|
||||||
.arg(t.backgroundAlt.name(), t.text.name(),
|
|
||||||
t.border.name(), t.hover.name());
|
|
||||||
m_themeCombo->setStyleSheet(comboStyle);
|
|
||||||
m_fontCombo->setStyleSheet(comboStyle);
|
|
||||||
|
|
||||||
// Dialog-wide stylesheet for everything else
|
|
||||||
setStyleSheet(QStringLiteral(
|
|
||||||
"QDialog { background: %1; }"
|
|
||||||
|
|
||||||
"QLineEdit {"
|
|
||||||
" background: %2; color: %3; border: 1px solid %4;"
|
|
||||||
" padding: 4px 8px; font-size: 12px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"QTreeWidget {"
|
|
||||||
" background: %2; color: %3; border: 1px solid %4;"
|
|
||||||
" font-size: 12px; outline: none;"
|
|
||||||
"}"
|
|
||||||
"QTreeWidget::item { padding: 3px 0; outline: none; }"
|
|
||||||
"QTreeWidget::item:selected { background: %5; color: %3; }"
|
|
||||||
"QTreeWidget::item:hover { background: %6; }"
|
|
||||||
|
|
||||||
"QGroupBox {"
|
|
||||||
" color: %3; border: 1px solid %4;"
|
|
||||||
" margin-top: 8px; padding: 12px 8px 8px 8px;"
|
|
||||||
" font-size: 12px; font-weight: bold;"
|
|
||||||
"}"
|
|
||||||
"QGroupBox::title {"
|
|
||||||
" subcontrol-origin: margin;"
|
|
||||||
" left: 8px; padding: 0 4px;"
|
|
||||||
"}"
|
|
||||||
|
|
||||||
"QLabel { color: %3; font-size: 12px; }"
|
|
||||||
|
|
||||||
"QCheckBox { color: %3; font-size: 12px; spacing: 6px; }"
|
|
||||||
|
|
||||||
"QPushButton {"
|
|
||||||
" background: %2; color: %3; border: 1px solid %4;"
|
|
||||||
" padding: 5px 16px; min-width: 70px; font-size: 12px;"
|
|
||||||
" outline: none;"
|
|
||||||
"}"
|
|
||||||
"QPushButton:hover { background: %6; }"
|
|
||||||
"QPushButton:pressed { background: %1; }"
|
|
||||||
"QPushButton:focus { outline: none; }")
|
|
||||||
.arg(t.background.name(), // %1
|
|
||||||
t.backgroundAlt.name(), // %2
|
|
||||||
t.text.name(), // %3
|
|
||||||
t.border.name(), // %4
|
|
||||||
t.selection.name(), // %5
|
|
||||||
t.hover.name())); // %6
|
|
||||||
|
|
||||||
// Install hover shadow on interactive widgets (not buttons — they use stylesheet hover)
|
|
||||||
for (auto* w : {static_cast<QWidget*>(m_search),
|
|
||||||
static_cast<QWidget*>(m_themeCombo),
|
|
||||||
static_cast<QWidget*>(m_fontCombo),
|
|
||||||
static_cast<QWidget*>(m_titleCaseCheck),
|
|
||||||
static_cast<QWidget*>(m_safeModeCheck),
|
|
||||||
static_cast<QWidget*>(m_autoMcpCheck)})
|
|
||||||
w->installEventFilter(this);
|
|
||||||
|
|
||||||
m_shadowColor = t.text;
|
|
||||||
m_shadowColor.setAlpha(80);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OptionsDialog::eventFilter(QObject* obj, QEvent* event) {
|
|
||||||
if (event->type() == QEvent::Enter) {
|
|
||||||
auto* w = qobject_cast<QWidget*>(obj);
|
|
||||||
if (w && !w->graphicsEffect()) {
|
|
||||||
auto* shadow = new QGraphicsDropShadowEffect(w);
|
|
||||||
shadow->setBlurRadius(12);
|
|
||||||
shadow->setOffset(0, 0);
|
|
||||||
shadow->setColor(m_shadowColor);
|
|
||||||
w->setGraphicsEffect(shadow);
|
|
||||||
}
|
|
||||||
} else if (event->type() == QEvent::Leave) {
|
|
||||||
auto* w = qobject_cast<QWidget*>(obj);
|
|
||||||
if (w)
|
|
||||||
w->setGraphicsEffect(nullptr);
|
|
||||||
}
|
|
||||||
return QDialog::eventFilter(obj, event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsResult OptionsDialog::result() const {
|
OptionsResult OptionsDialog::result() const {
|
||||||
@@ -272,6 +181,7 @@ OptionsResult OptionsDialog::result() const {
|
|||||||
r.themeIndex = m_themeCombo->currentIndex();
|
r.themeIndex = m_themeCombo->currentIndex();
|
||||||
r.fontName = m_fontCombo->currentText();
|
r.fontName = m_fontCombo->currentText();
|
||||||
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
||||||
|
r.showIcon = m_showIconCheck->isChecked();
|
||||||
r.safeMode = m_safeModeCheck->isChecked();
|
r.safeMode = m_safeModeCheck->isChecked();
|
||||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "themes/theme.h"
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QTreeWidget>
|
#include <QTreeWidget>
|
||||||
@@ -7,7 +6,6 @@
|
|||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QColor>
|
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -15,6 +13,7 @@ struct OptionsResult {
|
|||||||
int themeIndex = 0;
|
int themeIndex = 0;
|
||||||
QString fontName;
|
QString fontName;
|
||||||
bool menuBarTitleCase = true;
|
bool menuBarTitleCase = true;
|
||||||
|
bool showIcon = false;
|
||||||
bool safeMode = false;
|
bool safeMode = false;
|
||||||
bool autoStartMcp = false;
|
bool autoStartMcp = false;
|
||||||
};
|
};
|
||||||
@@ -26,9 +25,6 @@ public:
|
|||||||
|
|
||||||
OptionsResult result() const;
|
OptionsResult result() const;
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void filterTree(const QString& text);
|
void filterTree(const QString& text);
|
||||||
static QStringList collectPageKeywords(QWidget* page);
|
static QStringList collectPageKeywords(QWidget* page);
|
||||||
@@ -39,11 +35,10 @@ private:
|
|||||||
QComboBox* m_themeCombo = nullptr;
|
QComboBox* m_themeCombo = nullptr;
|
||||||
QComboBox* m_fontCombo = nullptr;
|
QComboBox* m_fontCombo = nullptr;
|
||||||
QCheckBox* m_titleCaseCheck = nullptr;
|
QCheckBox* m_titleCaseCheck = nullptr;
|
||||||
|
QCheckBox* m_showIconCheck = nullptr;
|
||||||
QCheckBox* m_safeModeCheck = nullptr;
|
QCheckBox* m_safeModeCheck = nullptr;
|
||||||
QCheckBox* m_autoMcpCheck = nullptr;
|
QCheckBox* m_autoMcpCheck = nullptr;
|
||||||
|
|
||||||
QColor m_shadowColor;
|
|
||||||
|
|
||||||
// searchable keywords per leaf tree item
|
// searchable keywords per leaf tree item
|
||||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||||
// tree item → stacked widget page index
|
// tree item → stacked widget page index
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
|||||||
clean.remove('&');
|
clean.remove('&');
|
||||||
|
|
||||||
if (titleCase) {
|
if (titleCase) {
|
||||||
|
action->setText("&" + clean.toUpper());
|
||||||
|
} else {
|
||||||
QString result;
|
QString result;
|
||||||
bool capitalizeNext = true;
|
bool capitalizeNext = true;
|
||||||
for (int i = 0; i < clean.length(); ++i) {
|
for (int i = 0; i < clean.length(); ++i) {
|
||||||
@@ -135,8 +137,6 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
action->setText("&" + result);
|
action->setText("&" + result);
|
||||||
} else {
|
|
||||||
action->setText("&" + clean.toUpper());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
251
tests/test_options_dialog.cpp
Normal file
251
tests/test_options_dialog.cpp
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#include <QtTest/QTest>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QTreeWidget>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QLabel>
|
||||||
|
#include "optionsdialog.h"
|
||||||
|
#include "themes/thememanager.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
// Helper: apply the global palette the same way main.cpp does
|
||||||
|
static void applyGlobalTheme(const Theme& theme) {
|
||||||
|
QPalette pal;
|
||||||
|
pal.setColor(QPalette::Window, theme.background);
|
||||||
|
pal.setColor(QPalette::WindowText, theme.text);
|
||||||
|
pal.setColor(QPalette::Base, theme.background);
|
||||||
|
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||||
|
pal.setColor(QPalette::Text, theme.text);
|
||||||
|
pal.setColor(QPalette::Button, theme.button);
|
||||||
|
pal.setColor(QPalette::ButtonText, theme.text);
|
||||||
|
pal.setColor(QPalette::Highlight, theme.selection);
|
||||||
|
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||||
|
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||||
|
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||||
|
pal.setColor(QPalette::Mid, theme.border);
|
||||||
|
pal.setColor(QPalette::Dark, theme.background);
|
||||||
|
pal.setColor(QPalette::Light, theme.textFaint);
|
||||||
|
pal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||||
|
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Light, theme.background);
|
||||||
|
|
||||||
|
qApp->setPalette(pal);
|
||||||
|
qApp->setStyleSheet(QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestOptionsDialog : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
|
||||||
|
void initTestCase() {
|
||||||
|
// Apply theme palette so dialog inherits real colors
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
applyGlobalTheme(tm.current());
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogCreatesAllWidgets() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
defaults.themeIndex = 0;
|
||||||
|
defaults.fontName = "JetBrains Mono";
|
||||||
|
defaults.menuBarTitleCase = true;
|
||||||
|
defaults.safeMode = false;
|
||||||
|
defaults.autoStartMcp = false;
|
||||||
|
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
// Core widgets exist
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
QVERIFY(tree);
|
||||||
|
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||||
|
QVERIFY(pages);
|
||||||
|
QCOMPARE(pages->count(), 3);
|
||||||
|
|
||||||
|
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||||
|
QVERIFY(themeCombo);
|
||||||
|
QVERIFY(themeCombo->count() >= 3);
|
||||||
|
|
||||||
|
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||||
|
QVERIFY(fontCombo);
|
||||||
|
QCOMPARE(fontCombo->count(), 2);
|
||||||
|
|
||||||
|
auto* showIconCheck = dlg.findChild<QCheckBox*>();
|
||||||
|
QVERIFY(showIconCheck);
|
||||||
|
|
||||||
|
auto* buttons = dlg.findChild<QDialogButtonBox*>();
|
||||||
|
QVERIFY(buttons);
|
||||||
|
QVERIFY(buttons->button(QDialogButtonBox::Ok));
|
||||||
|
QVERIFY(buttons->button(QDialogButtonBox::Cancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
void resultReflectsInput() {
|
||||||
|
OptionsResult input;
|
||||||
|
input.themeIndex = 1;
|
||||||
|
input.fontName = "Consolas";
|
||||||
|
input.menuBarTitleCase = false;
|
||||||
|
input.safeMode = true;
|
||||||
|
input.autoStartMcp = true;
|
||||||
|
|
||||||
|
OptionsDialog dlg(input);
|
||||||
|
auto r = dlg.result();
|
||||||
|
|
||||||
|
QCOMPARE(r.themeIndex, 1);
|
||||||
|
QCOMPARE(r.fontName, QString("Consolas"));
|
||||||
|
QCOMPARE(r.menuBarTitleCase, false);
|
||||||
|
QCOMPARE(r.safeMode, true);
|
||||||
|
QCOMPARE(r.autoStartMcp, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void noStyleSheetOnDialog() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
// Dialog itself must have no stylesheet override
|
||||||
|
QVERIFY(dlg.styleSheet().isEmpty());
|
||||||
|
|
||||||
|
// Combo boxes must have no stylesheet override
|
||||||
|
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||||
|
QVERIFY(themeCombo->styleSheet().isEmpty());
|
||||||
|
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||||
|
QVERIFY(fontCombo->styleSheet().isEmpty());
|
||||||
|
|
||||||
|
// No child widget should have a stylesheet set
|
||||||
|
for (auto* child : dlg.findChildren<QWidget*>()) {
|
||||||
|
QVERIFY2(child->styleSheet().isEmpty(),
|
||||||
|
qPrintable(QString("Widget %1 (%2) has unexpected stylesheet: %3")
|
||||||
|
.arg(child->objectName(),
|
||||||
|
child->metaObject()->className(),
|
||||||
|
child->styleSheet())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void highlightColorDiffersFromBackground() {
|
||||||
|
// Verify the palette Highlight is distinguishable from Window background
|
||||||
|
// This is the root cause of broken hover: if they're the same, hover is invisible
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto themes = tm.themes();
|
||||||
|
for (const auto& theme : themes) {
|
||||||
|
QVERIFY2(theme.selection != theme.background,
|
||||||
|
qPrintable(QString("Theme '%1': selection == background (%2)")
|
||||||
|
.arg(theme.name, theme.background.name())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void paletteHighlightIsSelection() {
|
||||||
|
// After applying theme, QPalette::Highlight must be theme.selection (not theme.hover)
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto& theme = tm.current();
|
||||||
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
QPalette pal = qApp->palette();
|
||||||
|
QCOMPARE(pal.color(QPalette::Highlight), theme.selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void treePageSwitching() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||||
|
QVERIFY(tree && pages);
|
||||||
|
|
||||||
|
// General is selected by default -> page 0
|
||||||
|
QCOMPARE(pages->currentIndex(), 0);
|
||||||
|
|
||||||
|
// Find "AI Features" item and select it
|
||||||
|
auto* envItem = tree->topLevelItem(0);
|
||||||
|
QVERIFY(envItem);
|
||||||
|
QTreeWidgetItem* aiItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
if (envItem->child(i)->text(0) == "AI Features") {
|
||||||
|
aiItem = envItem->child(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(aiItem);
|
||||||
|
tree->setCurrentItem(aiItem);
|
||||||
|
QCOMPARE(pages->currentIndex(), 1);
|
||||||
|
|
||||||
|
// Switch back to General
|
||||||
|
QTreeWidgetItem* generalItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
if (envItem->child(i)->text(0) == "General") {
|
||||||
|
generalItem = envItem->child(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(generalItem);
|
||||||
|
tree->setCurrentItem(generalItem);
|
||||||
|
QCOMPARE(pages->currentIndex(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void searchFilterHidesItems() {
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
|
||||||
|
auto* search = dlg.findChild<QLineEdit*>();
|
||||||
|
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||||
|
QVERIFY(search && tree);
|
||||||
|
|
||||||
|
auto* envItem = tree->topLevelItem(0);
|
||||||
|
QVERIFY(envItem);
|
||||||
|
|
||||||
|
// All children visible initially
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i)
|
||||||
|
QVERIFY(!envItem->child(i)->isHidden());
|
||||||
|
|
||||||
|
// Search for "MCP" - should hide General, show AI Features
|
||||||
|
search->setText("MCP");
|
||||||
|
QTreeWidgetItem* generalItem = nullptr;
|
||||||
|
QTreeWidgetItem* aiItem = nullptr;
|
||||||
|
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||||
|
auto* child = envItem->child(i);
|
||||||
|
if (child->text(0) == "General") generalItem = child;
|
||||||
|
if (child->text(0) == "AI Features") aiItem = child;
|
||||||
|
}
|
||||||
|
QVERIFY(generalItem && aiItem);
|
||||||
|
QVERIFY(generalItem->isHidden());
|
||||||
|
QVERIFY(!aiItem->isHidden());
|
||||||
|
|
||||||
|
// Clear search - all visible again
|
||||||
|
search->setText("");
|
||||||
|
QVERIFY(!generalItem->isHidden());
|
||||||
|
QVERIFY(!aiItem->isHidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogInheritsPalette() {
|
||||||
|
auto& tm = ThemeManager::instance();
|
||||||
|
const auto& theme = tm.current();
|
||||||
|
applyGlobalTheme(theme);
|
||||||
|
|
||||||
|
OptionsResult defaults;
|
||||||
|
OptionsDialog dlg(defaults);
|
||||||
|
dlg.show();
|
||||||
|
QTest::qWaitForWindowExposed(&dlg);
|
||||||
|
|
||||||
|
// Dialog's effective palette should match the app palette
|
||||||
|
QPalette dlgPal = dlg.palette();
|
||||||
|
QPalette appPal = qApp->palette();
|
||||||
|
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Window), appPal.color(QPalette::Window));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::WindowText), appPal.color(QPalette::WindowText));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Highlight), appPal.color(QPalette::Highlight));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::Button), appPal.color(QPalette::Button));
|
||||||
|
QCOMPARE(dlgPal.color(QPalette::ButtonText), appPal.color(QPalette::ButtonText));
|
||||||
|
|
||||||
|
// Highlight must be visible against background
|
||||||
|
QVERIFY(dlgPal.color(QPalette::Highlight) != dlgPal.color(QPalette::Window));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_MAIN(TestOptionsDialog)
|
||||||
|
#include "test_options_dialog.moc"
|
||||||
@@ -325,7 +325,7 @@ private slots:
|
|||||||
// Verify it's not all zeros (the old failure mode)
|
// Verify it's not all zeros (the old failure mode)
|
||||||
bool allZero = true;
|
bool allZero = true;
|
||||||
for (int i = 0; i < data.size(); ++i) {
|
for (int i = 0; i < data.size(); ++i) {
|
||||||
if (data[i] != 0) { allZero = false; break; }
|
if (data[i] != '\0') { allZero = false; break; }
|
||||||
}
|
}
|
||||||
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
|
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user