diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04eff81..f36805e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -151,7 +151,7 @@ jobs: path: neopdf_capi-${{ matrix.target }}.tar.gz publish-release: - needs: [capi-macos, cli-macos, capi-linux, cli-linux] + needs: [capi-macos, cli-macos, capi-linux, cli-linux, gui-macos, gui-linux] runs-on: ubuntu-latest if: "startsWith(github.ref, 'refs/tags/')" steps: @@ -166,6 +166,77 @@ jobs: find artifacts -name 'neopdf_*' ! -name '*.whl' -type f -exec gh release upload v${version} {} + gh release edit v${version} --draft=false + gui-macos: + needs: capi-macos + strategy: + matrix: + include: + - os: macos-13 + target: x86_64-apple-darwin + - os: macos-14 + target: aarch64-apple-darwin + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Install Qt + run: brew install qt6 + - name: Set Qt path + run: echo "CMAKE_PREFIX_PATH=$(brew --prefix qt6)" >> $GITHUB_ENV + - name: Download capi artifact + uses: actions/download-artifact@v4 + with: + name: neopdf_capi-${{ matrix.target }} + path: neopdf_capi_artifact + - name: Build and package + run: | + mkdir capi_install + tar -xzf neopdf_capi_artifact/neopdf_capi-${{ matrix.target }}.tar.gz -C capi_install + export CARGO_C_INSTALL_PREFIX=$(pwd)/capi_install + cmake -S neopdf_gui -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$(pwd)/install -DCMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH + cmake --build build --config Release + cmake --install build + cd install + cpack + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: neopdf_gui-${{ matrix.target }} + path: install/*.dmg + + gui-linux: + needs: capi-linux + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64-unknown-linux-gnu] + steps: + - uses: actions/checkout@v4 + - name: Install Qt and build essentials + run: | + sudo apt-get update + sudo apt-get install -y build-essential qt6-base-dev qt6-charts-dev + - name: Download capi artifact + uses: actions/download-artifact@v4 + with: + name: neopdf_capi-${{ matrix.target }} + path: neopdf_capi_artifact + - name: Build and package + run: | + mkdir capi_install + tar -xzf neopdf_capi_artifact/neopdf_capi-${{ matrix.target }}.tar.gz -C capi_install + export CARGO_C_INSTALL_PREFIX=$(pwd)/capi_install + cmake -S neopdf_gui -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$(pwd)/install + cmake --build build --config Release + cmake --install build + cd install + cpack + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: neopdf_gui-${{ matrix.target }} + path: install/*.tar.gz + + publish-crates: runs-on: ubuntu-latest container: ghcr.io/qcdlab/neopdf-container:latest diff --git a/.gitignore b/.gitignore index 31219be..9a78fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/neopdf_gui/build/ /neopdf_capi/docs/build/ /neopdf_capi/docs/xml/ /neopdf_capi/docs/html/ diff --git a/maintainer/make-release.sh b/maintainer/make-release.sh index aa3227b..21e162f 100755 --- a/maintainer/make-release.sh +++ b/maintainer/make-release.sh @@ -61,6 +61,10 @@ sed -i \ Cargo.toml git add Cargo.toml +# update GUI version +sed -i "s/^project(neopdf_gui VERSION .*)/project(neopdf_gui VERSION ${version})/" neopdf_gui/CMakeLists.txt +git add neopdf_gui/CMakeLists.txt + echo ">>> Updating Cargo.lock ..." # update explicit version for `neopdf_tmdlib` in `neopdf_cli` diff --git a/neopdf_gui/CMakeLists.txt b/neopdf_gui/CMakeLists.txt new file mode 100644 index 0000000..d24d60b --- /dev/null +++ b/neopdf_gui/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.16) +project(neopdf_gui VERSION 0.1.0) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Charts) + +set(CARGO_C_INSTALL_PREFIX "$ENV{CARGO_C_INSTALL_PREFIX}") +if(DEFINED CARGO_C_INSTALL_PREFIX AND NOT CARGO_C_INSTALL_PREFIX STREQUAL "") + list(APPEND NEOPDF_CAPI_INCLUDE_SEARCH_PATHS "${CARGO_C_INSTALL_PREFIX}/include/neopdf_capi") + list(APPEND NEOPDF_CAPI_LIBRARY_SEARCH_PATHS "${CARGO_C_INSTALL_PREFIX}/lib") +else() + message(FATAL_ERROR "CARGO_C_INSTALL_PREFIX must be specified.") +endif() + +find_path( + NEOPDF_CAPI_INCLUDE_DIR + NAMES NeoPDF.hpp neopdf_capi.h + PATHS ${NEOPDF_CAPI_INCLUDE_SEARCH_PATHS} + NO_DEFAULT_PATH +) +if(NOT NEOPDF_CAPI_INCLUDE_DIR) + message(FATAL_ERROR "Could not find NeoPDF.hpp/neopdf_capi.h.") +endif() + +find_library( + NEOPDF_CAPI_LIBRARY + NAMES neopdf_capi + PATHS ${NEOPDF_CAPI_LIBRARY_SEARCH_PATHS} + NO_DEFAULT_PATH +) +if(NOT NEOPDF_CAPI_LIBRARY) + message(FATAL_ERROR "Could not find libneopdf_capi.") +endif() + +message(STATUS "Found neopdf_capi library: ${NEOPDF_CAPI_LIBRARY}") +message(STATUS "Found neopdf_capi include dir: ${NEOPDF_CAPI_INCLUDE_DIR}") + +add_executable(neopdf_gui + main.cpp + MainWindow.cpp + MainWindow.hpp +) + +if(APPLE) + set_target_properties(neopdf_gui PROPERTIES + MACOSX_BUNDLE TRUE + ) +endif() + +target_link_libraries(neopdf_gui + PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Charts +) + +target_include_directories(neopdf_gui + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${NEOPDF_CAPI_INCLUDE_DIR} +) + +target_link_libraries(neopdf_gui + PRIVATE + ${NEOPDF_CAPI_LIBRARY} +) + +target_compile_definitions(neopdf_gui PRIVATE QT_NO_FILESYSTEM) + +install(TARGETS neopdf_gui + BUNDLE DESTINATION . + RUNTIME DESTINATION bin +) + +if(WIN32) + set(QT_DEPLOY_TOOL windeployqt) +elseif(APPLE) + set(QT_DEPLOY_TOOL macdeployqt) +endif() + +if(QT_DEPLOY_TOOL) + find_program(QT_DEPLOY_TOOL_PATH ${QT_DEPLOY_TOOL} HINTS ${Qt6_BIN_DIR}) + if(QT_DEPLOY_TOOL_PATH) + if(WIN32) + set(APP_PATH "\"${CMAKE_INSTALL_PREFIX}/bin/neopdf_gui.exe\"") + elseif(APPLE) + set(APP_PATH "\"${CMAKE_INSTALL_PREFIX}/neopdf_gui.app\"") + endif() + install(CODE "execute_process(COMMAND ${QT_DEPLOY_TOOL_PATH} ${APP_PATH})") + endif() +endif() + +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_GENERATOR "TGZ") +if(APPLE) + set(CPACK_GENERATOR "DragNDrop") +endif() + +include(CPack) diff --git a/neopdf_gui/MainWindow.cpp b/neopdf_gui/MainWindow.cpp new file mode 100644 index 0000000..a6ee2fb --- /dev/null +++ b/neopdf_gui/MainWindow.cpp @@ -0,0 +1,395 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "MainWindow.hpp" +#include "NeoPDF.hpp" + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { + setupUI(); + setWindowTitle("NeoPDF"); + resize(1200, 800); +} + +MainWindow::~MainWindow() {} + +void MainWindow::setupUI() { + centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + mainLayout = new QHBoxLayout(centralWidget); + + // --- Controls Panel --- + controlsLayout = new QVBoxLayout(); + + // PDF Set Management + setSelectionGroup = new QGroupBox("PDF Sets"); + setSelectionLayout = new QVBoxLayout(); + setListWidget = new QListWidget(); + setListWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + addSetButton = new QPushButton("Add Set"); + connect(addSetButton, &QPushButton::clicked, this, + &MainWindow::onAddSetButtonClicked); + connect(setListWidget->selectionModel(), + &QItemSelectionModel::selectionChanged, this, + &MainWindow::onSelectionSetChanged); + + setSelectionLayout->addWidget(setListWidget); + setSelectionLayout->addWidget(addSetButton); + clearSetsButton = new QPushButton("Clear All"); + setSelectionLayout->addWidget(clearSetsButton); + connect(clearSetsButton, &QPushButton::clicked, this, + &MainWindow::onClearSetsButtonClicked); + setSelectionGroup->setLayout(setSelectionLayout); + + // Plotting Parameters + plotParamsGroup = new QGroupBox("Plot Parameters"); + plotParamsLayout = new QFormLayout(); + + xAxisVarCombo = new QComboBox(); + connect(xAxisVarCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &MainWindow::onXAxisVarChanged); + + pidCombo = new QComboBox(); + pidCombo->addItem("g (21)", 21); + pidCombo->addItem("u (2)", 2); + pidCombo->addItem("d (1)", 1); + pidCombo->addItem("s (3)", 3); + pidCombo->addItem("c (4)", 4); + pidCombo->addItem("b (5)", 5); + pidCombo->addItem("t (6)", 6); + pidCombo->addItem("ubar (-2)", -2); + pidCombo->addItem("dbar (-1)", -1); + pidCombo->addItem("sbar (-3)", -3); + pidCombo->addItem("cbar (-4)", -4); + pidCombo->addItem("bbar (-5)", -5); + pidCombo->addItem("tbar (-6)", -6); + pidCombo->setCurrentIndex(0); // Default to gluon + + m_paramInfos.append({NEOPDF_SUBGRID_PARAMS_NUCLEONS, "Nucleon (A)", nullptr, + nullptr, false, 1.0, "1.0"}); + m_paramInfos.append({NEOPDF_SUBGRID_PARAMS_ALPHAS, "alpha_s", nullptr, + nullptr, false, 0.118, "0.118"}); + m_paramInfos.append( + {NEOPDF_SUBGRID_PARAMS_XI, "xi", nullptr, nullptr, false, 0.0, "0.0"}); + m_paramInfos.append({NEOPDF_SUBGRID_PARAMS_DELTA, "delta", nullptr, nullptr, + false, 0.0, "0.0"}); + m_paramInfos.append( + {NEOPDF_SUBGRID_PARAMS_KT, "kt", nullptr, nullptr, false, 0.0, "0.0"}); + m_paramInfos.append({NEOPDF_SUBGRID_PARAMS_MOMENTUM, "x", nullptr, nullptr, + false, 0.1, "0.1"}); + m_paramInfos.append({NEOPDF_SUBGRID_PARAMS_SCALE, "Q2", nullptr, nullptr, + false, 100.0, "100.0"}); + + plotParamsLayout->addRow("X-axis variable:", xAxisVarCombo); + plotParamsLayout->addRow("PID:", pidCombo); + + for (auto &info : m_paramInfos) { + info.widget = new QLineEdit(info.default_text); + info.label = new QLabel("Fixed " + info.name + " value:"); + plotParamsLayout->addRow(info.label, info.widget); + info.widget->setVisible(false); + info.label->setVisible(false); + } + + rangeMinEdit = new QLineEdit("1e-5"); + rangeMaxEdit = new QLineEdit("1.0"); + pointsEdit = new QLineEdit("100"); + xAxisLogCheck = new QCheckBox("Logarithmic X-axis"); + yAxisLogCheck = new QCheckBox("Logarithmic Y-axis"); + + plotParamsLayout->addRow("Plot Range Min:", rangeMinEdit); + plotParamsLayout->addRow("Plot Range Max:", rangeMaxEdit); + plotParamsLayout->addRow("Number of Points:", pointsEdit); + plotParamsLayout->addRow(xAxisLogCheck); + plotParamsLayout->addRow(yAxisLogCheck); + + plotParamsGroup->setLayout(plotParamsLayout); + + plotButton = new QPushButton("Plot"); + connect(plotButton, &QPushButton::clicked, this, + &MainWindow::onPlotButtonClicked); + + controlsLayout->addWidget(setSelectionGroup); + controlsLayout->addWidget(plotParamsGroup); + controlsLayout->addWidget(plotButton); + controlsLayout->addStretch(); + + chartView = new QChartView(); + chartView->setRenderHint(QPainter::Antialiasing); + + mainLayout->addLayout(controlsLayout, 1); + mainLayout->addWidget(chartView, 3); +} + +void MainWindow::onAddSetButtonClicked() { + bool ok; + QString setName = + QInputDialog::getText(this, tr("Add PDF Set"), tr("PDF set name:"), + QLineEdit::Normal, "", &ok); + if (ok && !setName.isEmpty()) { + QListWidgetItem *item = new QListWidgetItem(setName); + item->setData(Qt::UserRole, setName); + setListWidget->addItem(item); + setListWidget->setCurrentItem(item); + } +} + +void MainWindow::onSelectionSetChanged() { + updateParametersUI(setListWidget->selectedItems()); +} + +void MainWindow::updateParametersUI(const QList &items) { + if (items.isEmpty()) { + for (auto &info : m_paramInfos) { + info.active = false; + info.widget->setVisible(false); + info.label->setVisible(false); + } + xAxisVarCombo->clear(); + return; + } + + QSet commonParams; + bool first = true; + + for (const auto &item : items) { + QSet itemParams; + try { + neopdf::NeoPDF pdf( + item->data(Qt::UserRole).toString().toStdString(), 0); + for (const auto &info : m_paramInfos) { + auto range = pdf.param_range(info.id); + if (range[0] < range[1]) { + itemParams.insert(info.id); + } + } + } catch (const std::exception &e) { + QMessageBox::warning(this, "Error Loading Set", + "Could not inspect " + item->text() + ":\n" + + e.what()); + continue; + } + + if (first) { + commonParams = itemParams; + first = false; + } else { + commonParams.intersect(itemParams); + } + } + + xAxisVarCombo->blockSignals(true); + xAxisVarCombo->clear(); + + for (auto &info : m_paramInfos) { + info.active = commonParams.contains(info.id); + info.widget->setVisible(info.active); + info.label->setVisible(info.active); + if (info.active) { + xAxisVarCombo->addItem(info.name, static_cast(info.id)); + } + } + + xAxisVarCombo->blockSignals(false); + onXAxisVarChanged(xAxisVarCombo->currentIndex()); +} + +void MainWindow::onXAxisVarChanged(int index) { + if (index < 0) + return; + + auto selected_id = static_cast( + xAxisVarCombo->itemData(index).toInt()); + + for (auto &info : m_paramInfos) { + if (info.active) { + info.widget->setEnabled(info.id != selected_id); + } + } +} + +void MainWindow::onClearSetsButtonClicked() { + setListWidget->clear(); + updateParametersUI({}); // Call with empty list to reset UI +} + +void MainWindow::onPlotButtonClicked() { + QList selectedItems = setListWidget->selectedItems(); + if (selectedItems.isEmpty()) { + QMessageBox::warning(this, "No PDF Set", + "Please select one or more PDF sets to plot."); + return; + } + if (xAxisVarCombo->currentIndex() < 0) { + QMessageBox::warning(this, "No variable selected", + "No common variables to plot. Please select " + "sets with compatible kinematics."); + return; + } + + bool ok; + auto xAxisVarId = + static_cast(xAxisVarCombo->currentData().toInt()); + int pid = pidCombo->currentData().toInt(); + + QMap fixed_values; + for (const auto &info : m_paramInfos) { + if (info.active && info.id != xAxisVarId) { + double val = info.widget->text().toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "Invalid Input", + "Invalid value for " + info.name); + return; + } + fixed_values[info.id] = val; + } + } + + double range_min = rangeMinEdit->text().toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "Invalid Input", "Invalid range min value."); + return; + } + double range_max = rangeMaxEdit->text().toDouble(&ok); + if (!ok) { + QMessageBox::warning(this, "Invalid Input", "Invalid range max value."); + return; + } + int n_points = pointsEdit->text().toInt(&ok); + if (!ok || n_points <= 1) { + QMessageBox::warning(this, "Invalid Input", + "Number of points must be an integer > 1."); + return; + } + + bool isXLog = xAxisLogCheck->isChecked(); + if (isXLog && range_min <= 0.0) { + QMessageBox::warning( + this, "Invalid Input", + "Minimum range for logarithmic X-axis must be positive."); + return; + } + bool isYLog = yAxisLogCheck->isChecked(); + + auto *chart = new QChart(); + auto *y_axis = isYLog ? static_cast(new QLogValueAxis()) + : static_cast(new QValueAxis()); + chart->addAxis(y_axis, Qt::AlignLeft); + + QList colors = {Qt::blue, Qt::red, Qt::green, Qt::cyan, + Qt::magenta, Qt::yellow, Qt::darkGray}; + int color_idx = 0; + + for (auto *item : selectedItems) { + QString setName = item->data(Qt::UserRole).toString(); + neopdf::NeoPDFs *pdfs = nullptr; + try { + pdfs = new neopdf::NeoPDFs(setName.toStdString()); + } catch (const std::exception &e) { + QMessageBox::warning(this, "Plotting Error", + "Could not load " + setName + ":\n" + + e.what()); + continue; + } + + auto *mean_series = new QLineSeries(); + mean_series->setName(setName + "+1std"); + auto *upper_series = new QLineSeries(); + auto *lower_series = new QLineSeries(); + + double step = + isXLog ? std::pow(range_max / range_min, 1.0 / (n_points - 1)) + : (range_max - range_min) / (n_points - 1); + + for (int i = 0; i < n_points; ++i) { + double x_val = + isXLog ? range_min * std::pow(step, i) : range_min + i * step; + + std::vector params; + for (const auto &info : m_paramInfos) { + if (info.active) { + params.push_back( + info.id == xAxisVarId ? x_val : fixed_values[info.id]); + } + } + + std::vector results; + results.reserve(pdfs->size()); + for (size_t j = 0; j < pdfs->size(); ++j) { + results.push_back(pdfs->at(j).xfxQ2_ND(pid, params)); + } + + double sum = std::accumulate(results.begin(), results.end(), 0.0); + double mean = sum / results.size(); + double sq_sum = + std::accumulate(results.begin(), results.end(), 0.0, + [mean](double acc, double val) { + return acc + (val - mean) * (val - mean); + }); + double std_dev = std::sqrt(sq_sum / results.size()); + + mean_series->append(x_val, mean); + upper_series->append(x_val, mean + std_dev); + lower_series->append(x_val, mean - std_dev); + } + delete pdfs; + + auto *area_series = new QAreaSeries(upper_series, lower_series); + + QColor color = colors[color_idx % colors.size()]; + QPen pen(color); + pen.setWidth(2); + mean_series->setPen(pen); + + QColor areaColor = color; + areaColor.setAlphaF(0.15); + area_series->setColor(areaColor); + area_series->setBorderColor(areaColor); + area_series->setName(""); + + chart->addSeries(area_series); + chart->addSeries(mean_series); + + mean_series->attachAxis(y_axis); + area_series->attachAxis(y_axis); + + color_idx++; + } + + auto *x_axis = isXLog ? static_cast(new QLogValueAxis()) + : static_cast(new QValueAxis()); + x_axis->setTitleText(xAxisVarCombo->currentText()); + chart->addAxis(x_axis, Qt::AlignBottom); + + for (auto *s : chart->series()) { + s->attachAxis(x_axis); + } + + y_axis->setTitleText("xf"); + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignBottom); + + for (auto *series : chart->series()) { + if (auto *areaSeries = qobject_cast(series)) { + for (auto *marker : chart->legend()->markers(areaSeries)) { + marker->setVisible(false); + } + } + } + + chartView->setChart(chart); +} diff --git a/neopdf_gui/MainWindow.hpp b/neopdf_gui/MainWindow.hpp new file mode 100644 index 0000000..c953c2d --- /dev/null +++ b/neopdf_gui/MainWindow.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include + +#include "neopdf_capi.h" + +class QFormLayout; +class QLabel; +class QListWidgetItem; +class QLineEdit; + +struct ParamInfo { + NeopdfSubgridParams id; + QString name; + QLineEdit *widget = nullptr; + QLabel *label = nullptr; + bool active = false; + double default_val = 0.0; + QString default_text; +}; + +class MainWindow : public QMainWindow { + Q_OBJECT + + public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + private slots: + void onPlotButtonClicked(); + void onAddSetButtonClicked(); + void onXAxisVarChanged(int index); + void onSelectionSetChanged(); + void onClearSetsButtonClicked(); + + private: + void setupUI(); + void updateParametersUI(const QList &items); + + QWidget *centralWidget; + QHBoxLayout *mainLayout; + QVBoxLayout *controlsLayout; + + QGroupBox *setSelectionGroup; + QVBoxLayout *setSelectionLayout; + QListWidget *setListWidget; + QPushButton *addSetButton; + QPushButton *clearSetsButton; + + QGroupBox *plotParamsGroup; + QFormLayout *plotParamsLayout; + QComboBox *xAxisVarCombo; + QComboBox *pidCombo; + + QVector m_paramInfos; + + QLineEdit *rangeMinEdit; + QLineEdit *rangeMaxEdit; + QLineEdit *pointsEdit; + QCheckBox *xAxisLogCheck; + QCheckBox *yAxisLogCheck; + QPushButton *plotButton; + + QChartView *chartView; +}; diff --git a/neopdf_gui/README.md b/neopdf_gui/README.md new file mode 100644 index 0000000..f9abf73 --- /dev/null +++ b/neopdf_gui/README.md @@ -0,0 +1,28 @@ +# NeoPDF GUI Plotter + +A C++/Qt6 application to plot and compare PDF sets using the `neopdf` library. + +## Dependencies + +- A C++ compiler (g++, clang, msvc) +- CMake (version 3.16 or higher) +- Qt6 (including the Charts module) +- C/C++ NeoPDF APIS (see documentation for installation) + +## How to Build + +1. **Configure with CMake:** + First, specify the path where the NeoPDf C/C++-APIs are installed with the + variable `CARGO_C_INSTALL_PREFIX`. Then from the `neopdf_gui` directory, run: + ```bash + cmake -B build -DCMAKE_BUILD_TYPE=Release + ``` + On macOS, you might need to manually specify the path to Qt as follows: + ```bash + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/6.9.1/lib/cmake/Qt6 + ``` + +2. **Build the application:** + ```bash + cmake --build build + ``` diff --git a/neopdf_gui/main.cpp b/neopdf_gui/main.cpp new file mode 100644 index 0000000..c1dafc9 --- /dev/null +++ b/neopdf_gui/main.cpp @@ -0,0 +1,11 @@ +#include +#include + +#include "MainWindow.hpp" + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + MainWindow mainWindow; + mainWindow.show(); + return app.exec(); +}