diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6bf401..df303b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -106,7 +106,7 @@ jobs: - name: Install dependencies run: | 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 run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release @@ -119,23 +119,37 @@ jobs: env: 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 + cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png + + # Create AppImage with Qt libs bundled + export QMAKE=$Qt6_DIR/bin/qmake + export LD_LIBRARY_PATH=$Qt6_DIR/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 + mv Reclass-*.AppImage Reclass-x86_64.AppImage + - name: Upload artifact uses: actions/upload-artifact@v4 if: always() with: name: Reclass-Linux-x64 - path: | - build/Reclass - build/ReclassMcpBridge - - - name: Package release tarball - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: | - mkdir -p release - cp build/Reclass release/ - cp build/ReclassMcpBridge release/ - cp -r build/themes release/ 2>/dev/null || true - tar czf linux64-reclass-latest.tar.gz -C release . + path: Reclass-x86_64.AppImage - name: Update linux64 release if: github.event_name == 'push' && github.ref == 'refs/heads/main' @@ -144,10 +158,11 @@ jobs: tag_name: latest-linux64 name: linux64 body: | - Linux x64 build from main branch. + Linux x64 AppImage build from main branch. Commit: ${{ github.sha }} + Run `chmod +x Reclass-x86_64.AppImage && ./Reclass-x86_64.AppImage` prerelease: true - files: linux64-reclass-latest.tar.gz + files: Reclass-x86_64.AppImage make_latest: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/deploy/Reclass.desktop b/deploy/Reclass.desktop new file mode 100644 index 0000000..ac92259 --- /dev/null +++ b/deploy/Reclass.desktop @@ -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 diff --git a/src/main.cpp b/src/main.cpp index eff957f..536c27a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -364,6 +364,8 @@ void MainWindow::createMenus() { file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", QKeySequence::Save, this, &MainWindow::saveFile); file->addAction(makeIcon(":/vsicons/save-as.svg"), "Save &As...", QKeySequence::SaveAs, this, &MainWindow::saveFileAs); file->addSeparator(); + file->addAction(makeIcon(":/vsicons/close.svg"), "&Close", QKeySequence(Qt::CTRL | Qt::Key_W), this, &MainWindow::closeFile); + file->addSeparator(); file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp); file->addSeparator(); m_mcpAction = file->addAction(QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server", this, &MainWindow::toggleMcp); @@ -838,6 +840,10 @@ void MainWindow::saveFileAs() { project_save(nullptr, true); } +void MainWindow::closeFile() { + project_close(); +} + void MainWindow::addNode() { auto* ctrl = activeController(); if (!ctrl) return; @@ -1392,6 +1398,10 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) { delete doc; return nullptr; } + + // Close all existing tabs so the project replaces the current state + m_mdiArea->closeAllSubWindows(); + auto* sub = createTab(doc); rebuildWorkspaceModel(); return sub; @@ -1437,11 +1447,36 @@ void MainWindow::createWorkspaceDock() { m_workspaceTree->setExpandsOnDoubleClick(false); 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(subVar.value()); + 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); addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock); m_workspaceDock->hide(); - connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) { auto structIdVar = index.data(Qt::UserRole + 1); uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0; diff --git a/src/mainwindow.h b/src/mainwindow.h index c886c19..9a899aa 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -31,7 +31,7 @@ private slots: void openFile(); void saveFile(); void saveFileAs(); - + void closeFile(); void addNode(); void removeNode();