#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "trainWindow.h" #include "parser.h" enum ElementState { NEUTRAL = 0, CORRECT, INCORRECT }; const auto listStyle = "QListView {" " background-color: white;" " border: 0px solid black;" "}" "QListView::item::first {" " padding: 5px 0 0 0;" "}" "QListView::item {" " color: black;" " padding: 5px;" " background-color: none;" "}" "QListView::item:selected {" " background-color: transparent;" " padding-left: 0px;" " margin-left: 0px;" "}" "QListView::item:hover {" " background-color: lightyellow;" " padding-left: 0px;" " margin-left: 0px;" "}"; class CustomItemDelegate : public QStyledItemDelegate { public: CustomItemDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} // Override the paint method to apply custom styling void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { painter->save(); Qt::GlobalColor color; auto colorIdx = index.data(Qt::UserRole + 1).toInt() ; switch (colorIdx) { case NEUTRAL: color = Qt::lightGray; break; case INCORRECT: color = Qt::red; break; case CORRECT: color = Qt::green; break; } painter->setOpacity(0.5); if (color) { painter->fillRect(option.rect, color); } painter->restore(); QStyledItemDelegate::paint(painter, option, index); } }; class MultiChoiceListView : public QListView { Q_OBJECT public: explicit MultiChoiceListView(QWidget *parent = nullptr) : QListView(parent) { setStyleSheet(listStyle); setAcceptDrops(true); setEditTriggers(QAbstractItemView::NoEditTriggers); setFocusPolicy(Qt::NoFocus); setSelectionMode(QAbstractItemView::NoSelection); } }; class OrderListView : public QListView { Q_OBJECT public: explicit OrderListView(QWidget *parent = nullptr) : QListView(parent) { setStyleSheet(listStyle); setAcceptDrops(true); setFocusPolicy(Qt::NoFocus); setEditTriggers(QAbstractItemView::NoEditTriggers); setDragDropMode(QAbstractItemView::InternalMove); } protected: void dropEvent(QDropEvent *event) override { QModelIndex sourceIndex = currentIndex(); QModelIndex targetIndex = indexAt(event->pos()); if (sourceIndex.isValid() && targetIndex.isValid()) { QStandardItemModel *model = qobject_cast(this->model()); if (model) { QVariant sourceData = model->data(sourceIndex); QVariant targetData = model->data(targetIndex); model->setData(sourceIndex, targetData); model->setData(targetIndex, sourceData); event->ignore(); } } else { event->ignore(); } } }; class MoveListView : public QListView { Q_OBJECT public: explicit MoveListView(QWidget *parent = nullptr) : QListView(parent) { setStyleSheet(listStyle); setAcceptDrops(true); setDragDropMode(QAbstractItemView::DragDrop); setDefaultDropAction(Qt::MoveAction); setEditTriggers(QAbstractItemView::NoEditTriggers); setFocusPolicy(Qt::NoFocus); } protected: void dragEnterEvent(QDragEnterEvent *event) override { event->acceptProposedAction(); } void dragMoveEvent(QDragMoveEvent *event) override { event->acceptProposedAction(); } void dropEvent(QDropEvent *event) override { QListView *source = qobject_cast(event->source()); QStandardItemModel *sourceModel = qobject_cast(source->model()); QStandardItemModel *targetModel = qobject_cast(this->model()); if (source != this) { QList items = sourceModel->takeRow(source->currentIndex().row()); targetModel->appendRow(items); } event->ignore(); } }; #include "trainWindow.moc" struct GroupView { QWidget widget; QVBoxLayout vWidget; QLabel label; QScrollArea itemScroll; QStandardItemModel itemModel; MoveListView itemList; }; #define ITEM_POOL_CHUNK 200 // Main components QMainWindow *trainWindow; QWidget *trainWidget; QVBoxLayout *vTrainWidget; CustomItemDelegate *itemDelegate; // Question QWidget *questionBox; QVBoxLayout *vQuestionBox; QLabel *lQuestionText; // Bottom buttons QVBoxLayout *vButtonBox; QWidget *actionButtons; QHBoxLayout *hButtons; QToolButton *btnPrev; QSpacerItem *leftSpacer; QToolButton *btnTriggerAnswer; QSpacerItem *rightSpacer; QToolButton *btnNext; // Answer question QLabel *answerText; // Multi-choice question QStandardItemModel *multiChoiceModel; QListView *multiChoiceList; // Order question OrderListView *orderList; QStandardItemModel *orderModel; // Group question QStandardItemModel *groupItemModel; QListView *groupItemList; QWidget *wGroupQuestion; QVBoxLayout *vGroups; std::vector groupModels; // Questions & State std::vector trainQuestions = std::vector(); int32_t currentQuestionIndex = -1; std::vector groupViews; PracticeAlgorithm practiceAlgoritm; std::default_random_engine rng; std::vector itemPool; QToolButton *btnNotRemembered; QToolButton *btnHard; QToolButton *btnMedium; QToolButton *btnEasy; QStandardItem* acquireItem() { if (itemPool.size() <= 0) { for (int i = 0; i < ITEM_POOL_CHUNK; ++i) { itemPool.push_back(new QStandardItem); } } auto item = itemPool.back(); itemPool.pop_back(); return item; } void releaseItem(QStandardItem** item) { itemPool.push_back(*item); (*item) = nullptr; } void releaseAllItems() { std::cout << std::format("Starting release, currently in the pool: {}", itemPool.size()) << std::endl; auto releaseAllInModel = [](QStandardItemModel *model) { auto itemCount = model->rowCount(); for (int i = 0; i < itemCount; ++i) { auto item = model->item(0); model->takeRow(item->row()); releaseItem(&item); } }; releaseAllInModel(multiChoiceModel); releaseAllInModel(groupItemModel); releaseAllInModel(orderModel); for (auto groupView: groupViews) { releaseAllInModel(&groupView->itemModel); } std::cout << std::format("All items released, currently in the pool: {}", itemPool.size()) << std::endl; } void hideQuestionElements() { lQuestionText->hide(); answerText->hide(); orderList->hide(); multiChoiceList->hide(); wGroupQuestion->hide(); } void setupAnswerQuestion(MultiElementQuestion *question) { lQuestionText->setText( QString::fromStdString(question->QuestionText) ); lQuestionText->show(); auto ss = std::stringstream(); for (auto answerEl: question->Choices) { ss << std::format("- {}", answerEl.Answer) << std::endl; } answerText->setText( QString::fromStdString(ss.str()) ); if (answerText->isVisible()) { answerText->hide(); } QObject::connect( btnTriggerAnswer, &QToolButton::clicked, [](bool checked) { answerText->show(); btnTriggerAnswer->hide(); } ); btnTriggerAnswer->show(); } void setupOrderQuestion(MultiElementQuestion *question) { lQuestionText->setText( QString::fromStdString(question->QuestionText) ); lQuestionText->show(); orderModel->clear(); auto shuffledAnswers = question->Choices; std::shuffle(shuffledAnswers.begin(), shuffledAnswers.end(), rng); for (auto answerEl: shuffledAnswers) { auto *item = acquireItem(); item->setData(NEUTRAL, Qt::UserRole + 1); item->setData(QVariant(), Qt::CheckStateRole); item->setCheckable(false); item->setText(QString::fromStdString(answerEl.Answer)); orderModel->appendRow(item); } orderList->show(); QObject::connect( btnTriggerAnswer, &QToolButton::clicked, [question](bool checked) { for (int i = 0; i < orderModel->rowCount(); ++i) { auto item = orderModel->item(i, 0); auto text = item->text(); auto isCorrect = question->Choices[i].Answer.compare(text.toStdString()) == 0; if (isCorrect) { item->setData(CORRECT, Qt::UserRole + 1); } else { item->setData(INCORRECT, Qt::UserRole + 1); } orderList->update(); } btnTriggerAnswer->hide(); if (practiceAlgoritm == SPACED) { btnNotRemembered->show(); btnHard->show(); btnMedium->show(); btnEasy->show(); } } ); btnTriggerAnswer->show(); } void setupMultiChoiceQuestion(MultiElementQuestion *question) { lQuestionText->setText( QString::fromStdString(question->QuestionText) ); lQuestionText->show(); multiChoiceModel->clear(); for (auto answerEl: question->Choices) { auto *item = acquireItem(); item->setText(QString::fromStdString(answerEl.Answer)); item->setData(NEUTRAL, Qt::UserRole + 1); item->setCheckable(true); item->setCheckState(Qt::CheckState::Unchecked); multiChoiceModel->appendRow(item); } multiChoiceList->show(); QObject::connect( btnTriggerAnswer, &QToolButton::clicked, [question](bool checked) { for (int i = 0; i < multiChoiceModel->rowCount(); ++i) { auto item = multiChoiceModel->item(i, 0); auto isCorrect = question->Choices[i].IsCorrect == (item->checkState() == Qt::Checked); if (isCorrect) { item->setData(CORRECT, Qt::UserRole + 1); } else { item->setData(INCORRECT, Qt::UserRole + 1); } multiChoiceList->update(); } btnTriggerAnswer->hide(); } ); btnTriggerAnswer->show(); } void setupGroupQuestion(GroupQuestion *question) { auto groupSpacer = new QSpacerItem( 50, 50, QSizePolicy::Minimum, QSizePolicy::Expanding ); lQuestionText->setText( QString::fromStdString(question->QuestionText) ); lQuestionText->show(); wGroupQuestion->show(); for (auto group: question->Groups) { for (auto itemText: group.elements) { auto *qItem = acquireItem(); qItem->setData(NEUTRAL, Qt::UserRole + 1); qItem->setCheckable(false); qItem->setData(QVariant(), Qt::CheckStateRole); qItem->setText(QString::fromStdString(itemText)); groupItemModel->appendRow(qItem); } } groupItemList->update(); auto makeGroup = []() { auto groupView = new GroupView; vGroups->addWidget(&groupView->label); vGroups->addWidget(&groupView->itemList); groupView->itemList.setModel(&groupView->itemModel); groupView->itemList.setMaximumHeight(100); groupView->itemList.setItemDelegate(itemDelegate); groupView->widget.setLayout(&groupView->vWidget); groupView->widget.layout()->addWidget(&groupView->label); groupView->widget.layout()->addWidget(&groupView->itemList); return groupView; }; for (int k = 0; k < groupViews.size(); k++) { groupViews[k]->widget.hide(); } for (int i = 0; i < question->Groups.size(); i++) { GroupView *groupView; if (i < groupViews.size()) { groupView = groupViews[i]; } else { groupView = makeGroup(); groupViews.push_back(groupView); } groupView->label.setText( QString::fromStdString(question->Groups[i].name) ); vGroups->addWidget(&groupView->widget); groupView->widget.show(); } vGroups->addItem(groupSpacer); QObject::connect( btnTriggerAnswer, &QToolButton::clicked, [question](bool checked) { for (int i = 0; i < groupItemModel->rowCount(); ++i) { auto item = groupItemModel->item(i, 0); item->setData(INCORRECT, Qt::UserRole + 1); } for (int i = 0; i < groupViews.size(); i++) { auto groupView = groupViews[i]; auto group = question->Groups[i]; for (int j = 0; j < groupView->itemModel.rowCount(); ++j) { auto item = groupView->itemModel.item(j, 0); auto itemText = item->text().toStdString(); bool found = false; for (int k = 0; k < group.elements.size(); ++k) { if (group.elements[k].compare(itemText) == 0) { found = true; break; } } if (found) { item->setData(CORRECT, Qt::UserRole + 1); } else { item->setData(INCORRECT, Qt::UserRole + 1); } } groupView->itemList.update(); } btnTriggerAnswer->hide(); } ); btnTriggerAnswer->show(); } void setupQuestion(Question *question) { hideQuestionElements(); releaseAllItems(); QObject::disconnect(btnTriggerAnswer, 0, 0, 0); if (auto *question = dynamic_cast(trainQuestions[currentQuestionIndex])) { btnNotRemembered->hide(); btnHard->hide(); btnMedium->hide(); btnEasy->hide(); switch (question->type) { case MultiElementType::Order: setupOrderQuestion(question); break; case MultiElementType::MultiChoice: setupMultiChoiceQuestion(question); break; case MultiElementType::Regular: setupAnswerQuestion(question); break; } } else if (auto *question = dynamic_cast(trainQuestions[currentQuestionIndex])) { setupGroupQuestion(question); } } void updatePaginationVisibility() { if (currentQuestionIndex == 0) { btnPrev->hide(); } else { btnPrev->show(); } if (currentQuestionIndex == trainQuestions.size() - 1) { btnNext->hide(); } else { btnNext->show(); } } void initiatePractice( std::vector questions, PracticeAlgorithm algorithm, time_t *trainedAt ) { auto now = std::chrono::system_clock::now(); time_t unix_timestamp = std::chrono::duration_cast( now.time_since_epoch() ).count(); trainQuestions = questions; if (questions.size() <= 0) { return; } currentQuestionIndex = 0; updatePaginationVisibility(); setupQuestion(trainQuestions[currentQuestionIndex]); practiceAlgoritm = algorithm; btnNotRemembered->hide(); btnHard->hide(); btnMedium->hide(); btnEasy->hide(); } void initTrainWindow() { unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); rng = std::default_random_engine(seed); trainWindow = new QMainWindow(); trainWidget = new QWidget(); vTrainWidget = new QVBoxLayout(); trainWidget->setLayout(vTrainWidget); trainWindow->setCentralWidget(trainWidget); trainWidget->setLayout(vTrainWidget); vTrainWidget->setAlignment(Qt::AlignCenter); trainWidget->setObjectName("answer-question-widget"); itemDelegate = new CustomItemDelegate(multiChoiceList); { // Make the question box. questionBox = new QWidget(); vQuestionBox = new QVBoxLayout(); questionBox->setLayout(vQuestionBox); questionBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); vQuestionBox->setAlignment(Qt::AlignCenter); } { // Make question text. lQuestionText = new QLabel(); lQuestionText->setText("What is the capital of Latvia?"); lQuestionText->setWordWrap(true); lQuestionText->setStyleSheet(QString( "QLabel {" "font-size: 20px;" "}" )); lQuestionText->setTextInteractionFlags(Qt::TextSelectableByMouse); vQuestionBox->addWidget(lQuestionText); lQuestionText->hide(); } { // Make multi-choice list. multiChoiceModel = new QStandardItemModel(); multiChoiceList = new MultiChoiceListView(); multiChoiceList->setModel(multiChoiceModel); multiChoiceList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); questionBox->layout()->addWidget(multiChoiceList); multiChoiceList->hide(); multiChoiceList->setItemDelegate(itemDelegate); } { // Make answer text. answerText = new QLabel(); answerText->setText("- Riga\n- Second line"); answerText->setWordWrap(true); answerText->setStyleSheet(QString( "QLabel {" "font-size: 15px;" "}" )); answerText->setTextInteractionFlags(Qt::TextSelectableByMouse); vQuestionBox->addWidget(answerText); answerText->hide(); } { // Make order list. orderModel = new QStandardItemModel(); orderList = new OrderListView(); orderList->setModel(orderModel); orderList->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); orderList->setItemDelegate(itemDelegate); // Connect to handle the drop event properly QObject::connect( orderModel, &QStandardItemModel::rowsMoved, [](const QModelIndex &, int sourceRow, int, const QModelIndex &, int destinationRow) { if (sourceRow != destinationRow - 1) { auto *movedItem = orderModel->takeItem(sourceRow); orderModel->insertRow(destinationRow > sourceRow ? destinationRow - 1 : destinationRow, movedItem); } } ); questionBox->layout()->addWidget(orderList); orderList->hide(); } { // Make buttons menu. There are left middle and right buttons. vButtonBox = new QVBoxLayout(); actionButtons = new QWidget(); hButtons = new QHBoxLayout(); btnPrev = new QToolButton(); leftSpacer = new QSpacerItem(50, 50, QSizePolicy::Expanding, QSizePolicy::Minimum); btnTriggerAnswer = new QToolButton(); btnNotRemembered = new QToolButton(); btnHard = new QToolButton(); btnMedium = new QToolButton(); btnEasy = new QToolButton(); btnNotRemembered->setText("Not remembered"); btnHard->setText("Hard"); btnMedium->setText("Medium"); btnEasy->setText("Easy"); btnTriggerAnswer = new QToolButton(); rightSpacer = new QSpacerItem(50, 50, QSizePolicy::Expanding, QSizePolicy::Minimum); btnNext = new QToolButton(); hButtons->addWidget(btnPrev); hButtons->addItem(leftSpacer); hButtons->addWidget(btnTriggerAnswer); hButtons->addWidget(btnNotRemembered); hButtons->addWidget(btnHard); QObject::connect(btnHard, &QToolButton::clicked, []() { const double hourCooldown = 25.0; auto question = trainQuestions[currentQuestionIndex]; question->Cooldown = hourCooldown; }); hButtons->addWidget(btnMedium); hButtons->addWidget(btnEasy); hButtons->addItem(rightSpacer); hButtons->addWidget(btnNext); vButtonBox->addWidget(actionButtons); actionButtons->setLayout(hButtons); btnPrev->setText("Previous"); QObject::connect(btnPrev, &QToolButton::clicked, []() { currentQuestionIndex--; if (currentQuestionIndex > -1) { setupQuestion(trainQuestions[currentQuestionIndex]); } updatePaginationVisibility(); }); btnTriggerAnswer->setText("Show answer"); btnNext->setText("Next"); QObject::connect(btnNext, &QToolButton::clicked, []() { switch (practiceAlgoritm) { case PRIMARY: currentQuestionIndex++; if (currentQuestionIndex < trainQuestions.size()) { setupQuestion(trainQuestions[currentQuestionIndex]); } updatePaginationVisibility(); break; case RANDOM: // TODO: implement break; case SPACED: // TODO: implement break; } }); questionBox->setObjectName("question-box"); actionButtons->setStyleSheet(QString( "QToolButton {" "padding: 4px 5px;" "font-size: 15px;" "}" )); } { // Make group question view. wGroupQuestion = new QWidget(); wGroupQuestion->setStyleSheet( "border:none;" ); auto hGroupQuestion = new QHBoxLayout(); wGroupQuestion->setLayout(hGroupQuestion); // Items on the left. auto itemScroll = new QScrollArea(); auto vItems = new QVBoxLayout(); itemScroll->setLayout(vItems); groupItemModel = new QStandardItemModel(); groupItemList = new MoveListView(); groupItemList->setItemDelegate(itemDelegate); groupItemList->setModel(groupItemModel); vItems->addWidget(groupItemList); hGroupQuestion->addWidget(itemScroll); // Items on the right. auto groupScroll = new QScrollArea(); auto wGroupContainer = new QWidget(); vGroups = new QVBoxLayout(); groupScroll->setWidget(wGroupContainer); groupScroll->setWidgetResizable(true); wGroupContainer->setLayout(vGroups); hGroupQuestion->addWidget(groupScroll); wGroupQuestion->setStyleSheet("padding: 0; margin: 0;"); wGroupQuestion->hide(); questionBox->layout()->addWidget(wGroupQuestion); } vTrainWidget->addWidget(questionBox); vTrainWidget->addWidget(actionButtons); }