mirror of
https://github.com/jorenchik/mdemory.git
synced 2026-03-22 00:26:21 +00:00
653 lines
17 KiB
C++
653 lines
17 KiB
C++
#include <cstdio>
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <qabstractbutton.h>
|
|
#include <qboxlayout.h>
|
|
#include <qlabel.h>
|
|
#include <qlayoutitem.h>
|
|
#include <qmainwindow.h>
|
|
#include <qnamespace.h>
|
|
#include <qobjectdefs.h>
|
|
#include <qsizepolicy.h>
|
|
#include <qtoolbutton.h>
|
|
#include <qwidget.h>
|
|
#include <qwindow.h>
|
|
#include <qwindowdefs.h>
|
|
#include <regex>
|
|
#include <string>
|
|
|
|
#include <QApplication>
|
|
#include <QMainWindow>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QToolButton>
|
|
#include <QFileSystemModel>
|
|
#include <QTreeView>
|
|
#include <QSplitter>
|
|
#include <QVariant>
|
|
#include <QMessageBox>
|
|
#include <QScrollArea>
|
|
#include <QSpacerItem>
|
|
#include <QFile>
|
|
#include <QTime>
|
|
#include <QRegularExpression>
|
|
#include <QStringList>
|
|
#include <QListView>
|
|
#include <QWindow>
|
|
#include <qabstractitemmodel.h>
|
|
|
|
#include "api.h"
|
|
#include "parser.h"
|
|
#include "trainWindow.h"
|
|
|
|
#define TEXT_LG = 20
|
|
#define ERROR_POOL_CHUNK 50
|
|
#define DISTANCE 2
|
|
#define PER_PAGE 8
|
|
#define MDEM_BACKGROUND "#F7F7F7"
|
|
|
|
struct Mdem {
|
|
QWidget wMdem;
|
|
QVBoxLayout vMdem;
|
|
QLabel wFrontText;
|
|
QWidget wFront;
|
|
QHBoxLayout hFront;
|
|
QWidget wBack;
|
|
QVBoxLayout hBack;
|
|
QVector<QLabel*> backLabels;
|
|
QToolButton showButton;
|
|
int labelCount;
|
|
};
|
|
|
|
struct ErrorView {
|
|
QWidget box;
|
|
QVBoxLayout layout;
|
|
QLabel label;
|
|
};
|
|
|
|
struct Page {
|
|
int start;
|
|
int end;
|
|
};
|
|
|
|
QString workingPath = "/home/jorenchik/Code/mdemory/memorybase";
|
|
std::string currentPath = "";
|
|
|
|
// Mdem list
|
|
QLabel *deckListLabel;
|
|
QVBoxLayout *hMdemScroll;
|
|
QList<Mdem*> mdems = QList<Mdem*>();
|
|
std::vector<Question*> questions = std::vector<Question*>();
|
|
QSpacerItem *mdemSpacer;
|
|
std::vector<ErrorView*> errorPool;
|
|
std::vector<ErrorView*> errorViews;
|
|
|
|
// Pagination
|
|
int currentPage = -1;
|
|
std::vector<Page> pages;
|
|
QList<QToolButton*> paginationButtons;
|
|
QToolButton* prevButton;
|
|
QToolButton* firstButton;
|
|
QToolButton* lastButton;
|
|
QToolButton* nextButton;
|
|
QLabel* paginationLabel;
|
|
|
|
// Mdem actions
|
|
QToolButton *load;
|
|
QToolButton *practice;
|
|
|
|
const std::regex lastPathElementExp = std::regex("(.+\\/)*(.+)");
|
|
|
|
void showBacklabels(Mdem *mdem) {
|
|
for (int i = 0; i < mdem->backLabels.size(); ++i) {
|
|
if (i < mdem->labelCount) {
|
|
if (!mdem->backLabels[i]->isVisible()) {
|
|
mdem->backLabels[i]->show();
|
|
}
|
|
} else {
|
|
if (mdem->backLabels[i]->isVisible()) {
|
|
mdem->backLabels[i]->hide();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Mdem* makeMdem() {
|
|
auto mdem = new Mdem;
|
|
mdem->wMdem.setLayout(&mdem->vMdem);
|
|
|
|
QString id = QString("mdem_%1").arg(1);
|
|
mdem->wMdem.setObjectName(id);
|
|
|
|
// Front
|
|
mdem->wFront.setMinimumHeight(60);
|
|
mdem->wFront.setLayout(&mdem->hFront);
|
|
mdem->wFront.setProperty("first", "true");
|
|
mdem->wMdem.setStyleSheet(QString(
|
|
"QWidget#%1 > QWidget {"
|
|
"border-right: 1px solid gray;"
|
|
"border-bottom: 1px solid gray;"
|
|
"border-left: 1px solid gray;"
|
|
"background: %2;"
|
|
"} "
|
|
"QWidget#%1 > QWidget[first=\"true\"] {"
|
|
"border-top: 1px solid gray;"
|
|
"}"
|
|
).arg(id, MDEM_BACKGROUND));
|
|
|
|
// Add Front Content
|
|
mdem->hFront.addWidget(&mdem->wFrontText);
|
|
mdem->hFront.addStretch(1);
|
|
|
|
mdem->showButton.setText("Show");
|
|
mdem->hFront.addWidget(&mdem->showButton);
|
|
|
|
// Back
|
|
QVBoxLayout *hBack = new QVBoxLayout();
|
|
mdem->wBack.setLayout(&mdem->hBack);
|
|
mdem->vMdem.addWidget(&mdem->wBack);
|
|
|
|
// Add Back Content
|
|
for (int i = 0; i < 20; ++i) {
|
|
// @Fix: back label pooling
|
|
QLabel *elBackText = new QLabel();
|
|
mdem->hBack.addWidget(elBackText);
|
|
mdem->backLabels.append(elBackText);
|
|
}
|
|
|
|
mdem->vMdem.addWidget(&mdem->wFront);
|
|
mdem->vMdem.addWidget(&mdem->wBack);
|
|
mdem->vMdem.setContentsMargins(0, 0, 0, 0);
|
|
mdem->vMdem.setSpacing(0);
|
|
|
|
mdem->wBack.hide();
|
|
mdem->wMdem.hide();
|
|
|
|
// Connect button to toggle view
|
|
QObject::connect(&mdem->showButton, &QToolButton::clicked, [mdem]() {
|
|
if (mdem->wBack.isVisible()) {
|
|
mdem->wBack.hide();
|
|
mdem->showButton.setText("Show");
|
|
} else {
|
|
mdem->wBack.show();
|
|
mdem->showButton.setText("Hide");
|
|
}
|
|
showBacklabels(mdem);
|
|
});
|
|
return mdem;
|
|
}
|
|
|
|
void CreateMdems(std::vector<Question*>& questions) {
|
|
hMdemScroll->removeItem(mdemSpacer);
|
|
|
|
for (Mdem *mdem : mdems) {
|
|
if (mdem->wMdem.isVisible()) {
|
|
mdem->wMdem.hide();
|
|
}
|
|
}
|
|
|
|
if (questions.size() > mdems.size()) {
|
|
auto amount = questions.size() - mdems.size();
|
|
for (size_t i = 0; i < amount; ++i) {
|
|
auto mdem = makeMdem();
|
|
mdems.append(mdem);
|
|
hMdemScroll->addWidget(&mdem->wMdem);
|
|
}
|
|
}
|
|
|
|
|
|
for (size_t i = 0; i < questions.size(); ++i) {
|
|
if (MultiElementQuestion* mw = dynamic_cast<MultiElementQuestion*>(questions[i])) {
|
|
mdems[i]->wFrontText.setText(
|
|
QString::fromStdString(mw->QuestionText)
|
|
);
|
|
auto choices = mw->Choices;
|
|
for (size_t k = 0; k < choices.size(); ++k) {
|
|
auto answer = choices[k].Answer;
|
|
switch (mw->type) {
|
|
case MultiElementType::Order:
|
|
answer = std::format("{}. {}", k + 1, answer);
|
|
break;
|
|
case MultiElementType::MultiChoice:
|
|
if (choices[k].IsCorrect) {
|
|
answer = std::format("+ {}", answer);
|
|
} else {
|
|
answer = std::format("- {}", answer);
|
|
}
|
|
break;
|
|
case MultiElementType::Regular:
|
|
answer = std::format("- {}", answer);
|
|
break;
|
|
}
|
|
if (k < mdems[i]->backLabels.size()) {
|
|
mdems[i]->backLabels[k]->setText(QString::fromStdString(answer));
|
|
} else {
|
|
auto label = new QLabel();
|
|
label->setText(QString::fromStdString(answer));
|
|
mdems[i]->backLabels.push_back(label);
|
|
mdems[i]->wBack.layout()->addWidget(label);
|
|
}
|
|
}
|
|
mdems[i]->labelCount = choices.size();
|
|
} else if (GroupQuestion* mw = dynamic_cast<GroupQuestion*>(questions[i])) {
|
|
mdems[i]->wFrontText.setText(
|
|
QString::fromStdString(mw->QuestionText)
|
|
);
|
|
auto groups = mw->Groups;
|
|
std::vector<std::string> elements;
|
|
for (size_t k = 0; k < groups.size(); ++k) {
|
|
auto answer = groups[k].name;
|
|
elements.push_back(std::format("- {}:", answer));
|
|
for (size_t l = 0; l < groups[k].elements.size(); ++l) {
|
|
elements.push_back(std::format(" - {}", groups[k].elements[l]));
|
|
}
|
|
}
|
|
for (size_t k = 0; k < elements.size(); ++k) {
|
|
if (k < mdems[i]->backLabels.size()) {
|
|
mdems[i]->backLabels[k]->setText(QString::fromStdString(elements[k]));
|
|
} else {
|
|
auto label = new QLabel();
|
|
label->setText(QString::fromStdString(elements[k]));
|
|
mdems[i]->backLabels.push_back(label);
|
|
mdems[i]->wBack.layout()->addWidget(label);
|
|
}
|
|
}
|
|
mdems[i]->labelCount = elements.size();
|
|
}
|
|
if (!mdems[i]->wMdem.isVisible()) {
|
|
mdems[i]->wMdem.show();
|
|
}
|
|
}
|
|
|
|
hMdemScroll->addItem(mdemSpacer);
|
|
}
|
|
|
|
void makePages() {
|
|
pages.clear();
|
|
auto len = questions.size();
|
|
for (int i = 0; i < (len / PER_PAGE) + 1; i++) {
|
|
auto startingIndex = PER_PAGE * i ;
|
|
auto amount = PER_PAGE;
|
|
if (i == questions.size() / PER_PAGE) {
|
|
amount = questions.size() % PER_PAGE;
|
|
}
|
|
pages.push_back(Page{startingIndex, startingIndex + amount});
|
|
}
|
|
}
|
|
|
|
void SwitchPage(int pageIdx) {
|
|
currentPage = pageIdx;
|
|
|
|
// Hide all pagination buttons
|
|
for (auto& button : paginationButtons) {
|
|
button->hide();
|
|
}
|
|
|
|
int l = 0;
|
|
char buffer[50];
|
|
snprintf(buffer, sizeof(buffer), "Page: %d", pageIdx + 1);
|
|
paginationLabel->setText(buffer);
|
|
|
|
// Hide widgets in mdems
|
|
for (auto& mdem : mdems) {
|
|
if (mdem->wBack.isVisible()) {
|
|
mdem->wBack.hide();
|
|
mdem->showButton.setText("Show");
|
|
}
|
|
}
|
|
|
|
// Update pagination buttons
|
|
for (int k = -DISTANCE; k <= DISTANCE; ++k) {
|
|
if (pageIdx + k >= 0 && pageIdx + k < pages.size()) {
|
|
auto button = paginationButtons[l];
|
|
snprintf(buffer, sizeof(buffer), "%d", pageIdx + k + 1);
|
|
button->setText(buffer);
|
|
|
|
if (pageIdx + k != pageIdx) {
|
|
button->show();
|
|
} else {
|
|
button->hide();
|
|
}
|
|
++l;
|
|
}
|
|
}
|
|
|
|
// Handle first and last buttons
|
|
if (pageIdx > 0 && pages.size() > 1) {
|
|
firstButton->show();
|
|
} else {
|
|
firstButton->hide();
|
|
}
|
|
|
|
if (pageIdx < pages.size() - 1 && pages.size() > 1) {
|
|
lastButton->show();
|
|
} else {
|
|
lastButton->hide();
|
|
}
|
|
|
|
// Handle next and previous buttons
|
|
if (!pages.empty() && currentPage < pages.size() - 1) {
|
|
nextButton->show();
|
|
} else {
|
|
nextButton->hide();
|
|
}
|
|
|
|
if (!pages.empty() && currentPage >= 1) {
|
|
prevButton->show();
|
|
} else {
|
|
prevButton->hide();
|
|
}
|
|
|
|
// Handle page slice
|
|
const Page& page = pages[pageIdx];
|
|
|
|
std::vector<Question*> pageSlice(questions.begin() + page.start, questions.begin() + page.end);
|
|
CreateMdems(pageSlice);
|
|
}
|
|
|
|
ErrorView *makeErrorView() {
|
|
auto errorView = new ErrorView;
|
|
errorView->box.setObjectName("error-box");
|
|
errorView->box.setLayout(&errorView->layout);
|
|
errorView->box.setMinimumHeight(30);
|
|
errorView->box.setStyleSheet(
|
|
QString(
|
|
"QWidget#error-box {"
|
|
"border: 1px solid red;"
|
|
"background: %2;"
|
|
"}"
|
|
).arg(MDEM_BACKGROUND)
|
|
);
|
|
errorView->layout.addWidget(&errorView->label);
|
|
errorView->label.setWordWrap(true);
|
|
return errorView;
|
|
};
|
|
|
|
ErrorView* acquireError() {
|
|
if (errorPool.size() <= 0) {
|
|
for (int i = 0; i < ERROR_POOL_CHUNK; ++i) {
|
|
auto errorView = makeErrorView();
|
|
errorPool.push_back(errorView);
|
|
hMdemScroll->addWidget(&errorView->box);
|
|
errorView->box.hide();
|
|
}
|
|
}
|
|
auto item = errorPool.back();
|
|
errorPool.pop_back();
|
|
errorViews.push_back(item);
|
|
std::cout << std::format("Acquired, current pool size: {}\n", errorPool.size());
|
|
return item;
|
|
}
|
|
|
|
void releaseError(ErrorView** item) {
|
|
errorPool.push_back(*item);
|
|
(*item) = nullptr;
|
|
std::cout << std::format("Released, current pool size: {}\n", errorPool.size());
|
|
}
|
|
|
|
void loadMdem() {
|
|
auto file = std::ifstream(currentPath);
|
|
std::string content;
|
|
if (file) {
|
|
std::stringstream buffer;
|
|
buffer << file.rdbuf();
|
|
content = buffer.str();
|
|
auto res = transpile(content, true);
|
|
for (auto question: questions) {
|
|
delete question;
|
|
}
|
|
questions.clear();
|
|
|
|
std::smatch matches;
|
|
auto filenameMatched = std::regex_search(currentPath, matches, lastPathElementExp);
|
|
auto filename = matches[2];
|
|
deckListLabel->setText(
|
|
QString::fromStdString(std::format("mdem: {}", filename.str()))
|
|
);
|
|
|
|
while (errorViews.size() > 0) {
|
|
auto errorView = errorViews.back();
|
|
errorViews.pop_back();
|
|
errorView->box.hide();
|
|
releaseError(&errorView);
|
|
}
|
|
|
|
if (res.error == "") {
|
|
|
|
time_t trainedAt;
|
|
if (res.value.lastTrainedAt == 0) {
|
|
trainedAt = 0;
|
|
} else {
|
|
trainedAt = res.value.lastTrainedAt + 3600 * 2;
|
|
}
|
|
std::cout << std::format("Last trained at: {}", trainedAt) << std::endl;
|
|
questions = res.value.questions;
|
|
makePages();
|
|
SwitchPage(0);
|
|
} else {
|
|
std::cout << std::format("Compilation error: {}", res.error) << std::endl;
|
|
|
|
// Show errors.
|
|
hMdemScroll->removeItem(mdemSpacer);
|
|
auto errorView = acquireError();
|
|
errorView->label.setText(
|
|
QString::fromStdString(
|
|
std::format(
|
|
"Error while transpiling {}: {} ({}:{})",
|
|
filename.str(),
|
|
res.error,
|
|
res.row,
|
|
res.column
|
|
)
|
|
)
|
|
);
|
|
errorView->box.show();
|
|
hMdemScroll->addWidget(&errorView->box);
|
|
|
|
hMdemScroll->addItem(mdemSpacer);
|
|
for (auto mdem: mdems) {
|
|
if (mdem->wMdem.isVisible()) {
|
|
mdem->wMdem.hide();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
std::cout << std::format("Could not open the file: {}", currentPath) << std::endl;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
QApplication app(argc, argv);
|
|
QMainWindow window;
|
|
|
|
QSplitter *hSplitter = new QSplitter();
|
|
|
|
|
|
// LeftSide
|
|
QWidget *leftWidget = new QWidget();
|
|
QVBoxLayout *leftLayout = new QVBoxLayout();
|
|
QWidget *leftTop = new QWidget();
|
|
QVBoxLayout *vLeftTop = new QVBoxLayout();
|
|
QLabel *mdemLabel = new QLabel("mdems");
|
|
QFileSystemModel *model = new QFileSystemModel();
|
|
QTreeView *mdemList = new QTreeView();
|
|
mdemLabel->setStyleSheet(
|
|
"font-size: 17px;"
|
|
"font-weight: 400;"
|
|
);
|
|
mdemSpacer = new QSpacerItem(50, 50, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
|
|
|
leftWidget->setLayout(leftLayout);
|
|
leftLayout->addWidget(leftTop);
|
|
leftTop->setLayout(vLeftTop);
|
|
leftTop->setMinimumSize(0, 40);
|
|
vLeftTop->addWidget(mdemLabel);
|
|
model->setRootPath(workingPath);
|
|
// Hide all columns except the first one
|
|
mdemList->setModel(model);
|
|
|
|
QObject::connect(
|
|
mdemList,
|
|
&QTreeView::doubleClicked,
|
|
[model](const QModelIndex &index) {
|
|
auto fileInfo = model->fileInfo(index);
|
|
currentPath = fileInfo.filePath().toStdString();
|
|
loadMdem();
|
|
}
|
|
);
|
|
|
|
QModelIndex rootIndex = model->index(
|
|
"/home/jorenchik/Code/mdemory/memorybase"
|
|
);
|
|
mdemList->setRootIndex(rootIndex);
|
|
for (int col = 1; col < model->columnCount(); ++col) {
|
|
mdemList->hideColumn(col);
|
|
}
|
|
model->setHeaderData(0, Qt::Horizontal, QObject::tr("Custom Name"));
|
|
leftLayout->addWidget(mdemList);
|
|
|
|
|
|
// DeckList
|
|
QLabel *deckLabel = new QLabel("decks");
|
|
QWidget *deckLabelBox = new QWidget();
|
|
QVBoxLayout *vDeckLabelBox = new QVBoxLayout();
|
|
QListView *deckList = new QListView();
|
|
deckLabel->setStyleSheet(
|
|
"font-size: 17px;"
|
|
"font-weight: 400;"
|
|
);
|
|
deckLabelBox->setLayout(vDeckLabelBox);
|
|
vDeckLabelBox->addWidget(deckLabel);
|
|
deckLabelBox->setMinimumSize(0, 40);
|
|
leftLayout->addWidget(deckLabelBox);
|
|
leftLayout->addWidget(deckList);
|
|
|
|
// RightSide
|
|
QWidget *rightWidget = new QWidget();
|
|
QVBoxLayout *rightLayout = new QVBoxLayout();
|
|
rightWidget->setLayout(rightLayout);
|
|
|
|
QWidget *top = new QWidget();
|
|
QHBoxLayout *hTop = new QHBoxLayout();
|
|
deckListLabel = new QLabel();
|
|
deckListLabel->setStyleSheet(
|
|
"font-size: 17px;"
|
|
"font-weight: 400;"
|
|
);
|
|
top->setMinimumSize(0, 40);
|
|
top->setLayout(hTop);
|
|
rightLayout->addWidget(top);
|
|
|
|
hTop->addWidget(deckListLabel);
|
|
hTop->addStretch(1);
|
|
|
|
load = new QToolButton();
|
|
practice = new QToolButton();
|
|
|
|
hTop->addWidget(load);
|
|
hTop->addWidget(practice);
|
|
|
|
// Buttons
|
|
load->setText("Load");
|
|
practice->setText("Practice");
|
|
QObject::connect(load, &QToolButton::clicked, &loadMdem);
|
|
|
|
// Mdems
|
|
QScrollArea *mdemScroll = new QScrollArea();
|
|
QWidget *mdemContainer = new QWidget();
|
|
hMdemScroll = new QVBoxLayout();
|
|
mdemScroll->setWidget(mdemContainer);
|
|
mdemScroll->setWidgetResizable(true);
|
|
mdemContainer->setLayout(hMdemScroll);
|
|
rightLayout->addWidget(mdemScroll);
|
|
hMdemScroll->addItem(mdemSpacer);
|
|
|
|
// Pagination
|
|
hSplitter->addWidget(leftWidget);
|
|
hSplitter->addWidget(rightWidget);
|
|
hSplitter->setStretchFactor(0, 1);
|
|
hSplitter->setStretchFactor(1, 3);
|
|
|
|
|
|
auto pagination = new QWidget();
|
|
auto hPagination = new QHBoxLayout();
|
|
pagination->setLayout(hPagination);
|
|
|
|
firstButton = new QToolButton();
|
|
firstButton->setText(QString::fromStdString("<<"));
|
|
hPagination->addWidget(firstButton);
|
|
firstButton->hide();
|
|
QObject::connect(firstButton, &QToolButton::clicked, [](bool checked) {
|
|
if (pages.size() > 0) {
|
|
SwitchPage(0);
|
|
}
|
|
});
|
|
|
|
prevButton = new QToolButton();
|
|
prevButton->setText(QString::fromStdString("<"));
|
|
hPagination->addWidget(prevButton);
|
|
prevButton->hide();
|
|
QObject::connect(prevButton, &QToolButton::clicked, [](bool checked) {
|
|
if (pages.size() > 0) {
|
|
SwitchPage(currentPage - 1);
|
|
}
|
|
});
|
|
|
|
for (int i = 0; i < PER_PAGE; i++) {
|
|
auto elButton = new QToolButton();
|
|
elButton->setText(QString("%1").arg(i+1));
|
|
hPagination->addWidget(elButton);
|
|
elButton->hide();
|
|
QObject::connect(elButton, &QToolButton::clicked, [elButton](bool checked) {
|
|
auto pageNum = std::stoi(elButton->text().toStdString().c_str());
|
|
auto pageIdx = pageNum - 1;
|
|
if (pageIdx < pages.size()) {
|
|
SwitchPage(pageIdx);
|
|
}
|
|
});
|
|
paginationButtons.push_back(elButton);
|
|
}
|
|
|
|
nextButton = new QToolButton();
|
|
nextButton->setText(QString::fromStdString(">"));
|
|
hPagination->addWidget(nextButton);
|
|
nextButton->hide();
|
|
QObject::connect(nextButton, &QToolButton::clicked, [](bool checked) {
|
|
if (pages.size() > 0) {
|
|
SwitchPage(currentPage + 1);
|
|
}
|
|
});
|
|
|
|
lastButton = new QToolButton();
|
|
lastButton->setText(QString::fromStdString(">>"));
|
|
hPagination->addWidget(lastButton);
|
|
lastButton->hide();
|
|
QObject::connect(lastButton, &QToolButton::clicked, [](bool checked) {
|
|
if (pages.size() > 0) {
|
|
SwitchPage(pages.size() - 1);
|
|
}
|
|
});
|
|
hPagination->addStretch(1);
|
|
paginationLabel = new QLabel();
|
|
hPagination->addWidget(paginationLabel);
|
|
rightLayout->addWidget(pagination);
|
|
|
|
initTrainWindow();
|
|
QObject::connect(
|
|
practice,
|
|
&QToolButton::clicked,
|
|
[](bool checked) {
|
|
trainWindow->show();
|
|
trainWindow->resize(600, 300);
|
|
setQuestions(questions);
|
|
}
|
|
);
|
|
|
|
window.setCentralWidget(hSplitter);
|
|
window.show();
|
|
|
|
return app.exec();
|
|
}
|