From d24f813f3f2a05c112e803e4256b53535895fc98 Mon Sep 17 00:00:00 2001 From: Jedidiah Barber Date: Wed, 14 Jul 2021 11:49:10 +1200 Subject: Initial mirror commit --- src/study/CardEditDialog.cpp | 119 +++++++++++ src/study/CardEditDialog.h | 42 ++++ src/study/CardSideView.cpp | 205 ++++++++++++++++++ src/study/CardSideView.h | 52 +++++ src/study/CardsStatusBar.cpp | 82 ++++++++ src/study/CardsStatusBar.h | 37 ++++ src/study/IStudyModel.cpp | 9 + src/study/IStudyModel.h | 35 ++++ src/study/IStudyWindow.cpp | 293 ++++++++++++++++++++++++++ src/study/IStudyWindow.h | 107 ++++++++++ src/study/NumberFrame.cpp | 40 ++++ src/study/NumberFrame.h | 29 +++ src/study/SpacedRepetitionModel.cpp | 390 +++++++++++++++++++++++++++++++++++ src/study/SpacedRepetitionModel.h | 79 +++++++ src/study/SpacedRepetitionWindow.cpp | 380 ++++++++++++++++++++++++++++++++++ src/study/SpacedRepetitionWindow.h | 89 ++++++++ src/study/StudyFileReader.cpp | 238 +++++++++++++++++++++ src/study/StudyFileReader.h | 44 ++++ src/study/StudyFileWriter.cpp | 68 ++++++ src/study/StudyFileWriter.h | 26 +++ src/study/StudyRecord.cpp | 129 ++++++++++++ src/study/StudyRecord.h | 62 ++++++ src/study/StudySettings.cpp | 93 +++++++++ src/study/StudySettings.h | 40 ++++ src/study/WarningPanel.cpp | 49 +++++ src/study/WarningPanel.h | 25 +++ src/study/WordDrillModel.cpp | 163 +++++++++++++++ src/study/WordDrillModel.h | 62 ++++++ src/study/WordDrillWindow.cpp | 180 ++++++++++++++++ src/study/WordDrillWindow.h | 46 +++++ 30 files changed, 3213 insertions(+) create mode 100644 src/study/CardEditDialog.cpp create mode 100644 src/study/CardEditDialog.h create mode 100644 src/study/CardSideView.cpp create mode 100644 src/study/CardSideView.h create mode 100644 src/study/CardsStatusBar.cpp create mode 100644 src/study/CardsStatusBar.h create mode 100644 src/study/IStudyModel.cpp create mode 100644 src/study/IStudyModel.h create mode 100644 src/study/IStudyWindow.cpp create mode 100644 src/study/IStudyWindow.h create mode 100644 src/study/NumberFrame.cpp create mode 100644 src/study/NumberFrame.h create mode 100644 src/study/SpacedRepetitionModel.cpp create mode 100644 src/study/SpacedRepetitionModel.h create mode 100644 src/study/SpacedRepetitionWindow.cpp create mode 100644 src/study/SpacedRepetitionWindow.h create mode 100644 src/study/StudyFileReader.cpp create mode 100644 src/study/StudyFileReader.h create mode 100644 src/study/StudyFileWriter.cpp create mode 100644 src/study/StudyFileWriter.h create mode 100644 src/study/StudyRecord.cpp create mode 100644 src/study/StudyRecord.h create mode 100644 src/study/StudySettings.cpp create mode 100644 src/study/StudySettings.h create mode 100644 src/study/WarningPanel.cpp create mode 100644 src/study/WarningPanel.h create mode 100644 src/study/WordDrillModel.cpp create mode 100644 src/study/WordDrillModel.h create mode 100644 src/study/WordDrillWindow.cpp create mode 100644 src/study/WordDrillWindow.h (limited to 'src/study') diff --git a/src/study/CardEditDialog.cpp b/src/study/CardEditDialog.cpp new file mode 100644 index 0000000..5dc47a3 --- /dev/null +++ b/src/study/CardEditDialog.cpp @@ -0,0 +1,119 @@ +#include "CardEditDialog.h" +#include "IStudyWindow.h" +#include "IStudyModel.h" +#include "../dictionary/Card.h" +#include "../dictionary/CardPack.h" +#include "../dictionary/Dictionary.h" +#include "../main-view/DictTableModel.h" +#include "../main-view/DictTableView.h" +#include "../main-view/CardFilterModel.h" +#include "../main-view/MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include + +CardEditDialog::CardEditDialog(Card* aCurCard, MainWindow* aMainWindow, IStudyWindow* aStudyWindow ): + QDialog( aStudyWindow ), m_cardEditModel( NULL ), m_cardEditView( NULL ), m_mainWindow( aMainWindow ) + { + DictTableModel* tableModel = aStudyWindow->studyModel()->getDictModel(); + if( !tableModel ) + return; + + m_cardEditModel = new CardFilterModel( tableModel ); + m_cardEditModel->setSourceModel( tableModel ); + + const CardPack* cardPack = static_cast(aCurCard->getCardPack()); + Q_ASSERT(cardPack); + m_dictionary = static_cast(cardPack->dictionary()); + Q_ASSERT( m_dictionary ); + foreach( const DicRecord* record, aCurCard->getSourceRecords() ) + { + int row = m_dictionary->indexOfRecord( const_cast( record ) ); + if( row > -1 ) + m_cardEditModel->addFilterRow( row ); + } + + m_cardEditView = new DictTableView( m_cardEditModel, this ); + m_cardEditView->setContextMenuPolicy( Qt::ActionsContextMenu ); + m_cardEditView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu ); + if( m_mainWindow ) + { + m_cardEditView->addActions(m_mainWindow->getContextMenuActions()); + m_cardEditView->verticalHeader()->addActions(m_mainWindow->getContextMenuActions()); + connect( m_cardEditView->selectionModel(), + SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + m_mainWindow, SLOT(updateSelectionActions()) ); + } + + QPushButton* dicWindowBtn = new QPushButton(tr("Go to dictionary window")); + connect( dicWindowBtn, SIGNAL(clicked()), SLOT(goToDictionaryWindow())); + QPushButton* closeBtn = new QPushButton(tr("Close")); + connect( closeBtn, SIGNAL(clicked()), SLOT(close())); + + QVBoxLayout* editLt = new QVBoxLayout; + editLt->addWidget( m_cardEditView ); + QHBoxLayout* buttonsLt = new QHBoxLayout; + buttonsLt->addStretch( 1 ); + buttonsLt->addWidget( dicWindowBtn ); + buttonsLt->addWidget( closeBtn ); + editLt->addLayout( buttonsLt ); + + setLayout( editLt ); + setWindowTitle( QString(tr("Edit card: ", "In title of card edit view")) + aCurCard->getName() ); + setWindowModality( Qt::WindowModal ); + closeBtn->setFocus(); + m_cardEditView->setCurrentIndex( m_cardEditModel->index(0, 0) ); + + QSettings settings; + QVariant posVar = settings.value("cardeditview-pos"); + if( !posVar.isNull() ) + move( posVar.toPoint() ); + resize( settings.value("cardeditview-size", QSize(CardEditViewWidth, CardEditViewHeight)).toSize() ); + } + +CardEditDialog::~CardEditDialog() + { + delete m_cardEditView; + delete m_cardEditModel; + } + +void CardEditDialog::closeEvent( QCloseEvent* event ) + { + QSettings settings; + settings.setValue("cardeditview-pos", pos()); + settings.setValue("cardeditview-size", size()); + event->accept(); + } + +bool CardEditDialog::event( QEvent* event ) + { + if( event->type() == QEvent::WindowActivate || event->type() == QEvent::WindowDeactivate ) + { + m_mainWindow->updateSelectionActions(); + return true; + } + return QDialog::event( event ); + } + +const DictTableView* CardEditDialog::cardEditView() const + { + if( isActiveWindow() ) + return m_cardEditView; + else + return NULL; + } + +void CardEditDialog::goToDictionaryWindow() + { + close(); + QModelIndex curProxyIndex = m_cardEditView->currentIndex(); + QModelIndex curSourceIndex = m_cardEditModel->mapToSource( curProxyIndex ); + if( !m_dictionary ) + return; + m_mainWindow->goToDictionaryRecord( m_dictionary, curSourceIndex.row() ); + } diff --git a/src/study/CardEditDialog.h b/src/study/CardEditDialog.h new file mode 100644 index 0000000..3eb3d1b --- /dev/null +++ b/src/study/CardEditDialog.h @@ -0,0 +1,42 @@ +#ifndef CARDEDITDIALOG_H +#define CARDEDITDIALOG_H + +#include +#include + +class Card; +class Dictionary; +class CardFilterModel; +class DictTableView; +class DictTableModel; +class MainWindow; +class IStudyWindow; + +class CardEditDialog : public QDialog +{ + Q_OBJECT + +public: + CardEditDialog(Card* aCurCard, MainWindow* aMainWindow, IStudyWindow* aStudyWindow); + ~CardEditDialog(); + + const DictTableView* cardEditView() const; + +protected: + void closeEvent( QCloseEvent* event ); + bool event( QEvent* event ); + +private slots: + void goToDictionaryWindow(); + +private: + static const int CardEditViewHeight = 130; + static const int CardEditViewWidth = 600; + + const Dictionary* m_dictionary; + CardFilterModel* m_cardEditModel; + DictTableView* m_cardEditView; + MainWindow* m_mainWindow; +}; + +#endif diff --git a/src/study/CardSideView.cpp b/src/study/CardSideView.cpp new file mode 100644 index 0000000..8956210 --- /dev/null +++ b/src/study/CardSideView.cpp @@ -0,0 +1,205 @@ +#include "CardSideView.h" +#include "../field-styles/FieldStyleFactory.h" +#include "../dictionary/CardPack.h" +#include "../dictionary/IDictionary.h" + +#include + +CardSideView::CardSideView( bool aMode ): + m_showMode(aMode), cardPack(NULL) +{ + setFrameStyle( QFrame::Plain | QFrame::Box ); + setAlignment( Qt::AlignCenter ); + setTextFormat( Qt::RichText ); + setWordWrap( true ); + setAutoFillBackground( true ); + setPalette( FieldStyleFactory::inst()->cardBgColor ); + newIcon = new QLabel(this); + newIcon->setPixmap(QPixmap(":/images/new-topright.png")); + updateNewIconPos(); + newIcon->hide(); +} + +void CardSideView::setEnabled( bool aEnabled ) + { + QLabel::setEnabled( aEnabled ); + setBackgroundVisible( aEnabled ); + } + +/** Cleans background and text */ +void CardSideView::setBackgroundVisible( bool aVisible ) + { + if( aVisible ) + QLabel::setEnabled( true ); + setAutoFillBackground( aVisible ); + setLineWidth( aVisible? 1 : 0 ); + if( !aVisible ) + setText(""); + } + +void CardSideView::setPack(const CardPack* pack) + { + cardPack = pack; + } + +void CardSideView::setQstAnsr( const QString aQuestion, const QStringList aAnswers ) + { + m_question = aQuestion; + m_answers = aAnswers; + updateText(); + } + +void CardSideView::setQuestion( const QString aQuestion ) + { + m_question = aQuestion; + updateText(); + } + +void CardSideView::setShowMode( bool aMode ) + { + m_showMode = aMode; + updateText(); + } + +void CardSideView::updateText() +{ + setText(getFormattedText()); +} + +QString CardSideView::getFormattedText() const +{ + if(!cardPack) + return QString(); + if(m_showMode == QstMode) + return getFormattedQuestion(); + else + return getFormattedAnswer(); +} + +QString CardSideView::getFormattedQuestion() const +{ + const Field* qstField = cardPack->getQuestionField(); + if(!qstField) + return QString(); + return formattedField(m_question, qstField->style()); +} + +QString CardSideView::getFormattedAnswer() const +{ + QStringList formattedAnswers; + int i = 0; + foreach(const Field* field, cardPack->getAnswerFields()) + { + QString text = getFormattedAnswerField(field, i); + if(i == 0) + text += "
"; + if(!text.isEmpty()) + formattedAnswers << text; + i++; + } + return formattedAnswers.join("
"); +} + +QString CardSideView::getFormattedAnswerField(const Field* field, int index) const +{ + if(!field) + return QString(); + if(index >= m_answers.size()) + return QString(); + if(m_answers[index].isEmpty()) + return QString(); + return formattedField(m_answers[index], field->style() ); +} + +QString CardSideView::formattedField( const QString aField, const QString aStyle ) const + { + Q_ASSERT( cardPack ); + QString text = static_cast(cardPack->dictionary())-> + extendImagePaths( aField ); + + FieldStyle fieldStyle = FieldStyleFactory::inst()->getStyle( aStyle ); + QString beginning("" + fieldStyle.prefix; + QString ending( fieldStyle.suffix + ""); + + // Highlight keywords inside plain text. Ignore tags. + if(m_showMode == AnsMode && fieldStyle.hasKeyword) + { + QString highlightedText; + int curPos = 0; + while( curPos < text.length() ) + { + QString curText; + int beginTagPos = text.indexOf( "<", curPos ); + if( beginTagPos > -1 ) + curText = text.mid( curPos, beginTagPos - curPos ); // copy plain text + else + curText = text.mid( curPos, -1 ); // copy until end of string + curText = highlightKeyword(curText, fieldStyle); + highlightedText += curText; + if( beginTagPos == -1 ) + break; + int endTagPos = text.indexOf( ">", beginTagPos ); + if( endTagPos > -1 ) + highlightedText += text.mid( beginTagPos, endTagPos - beginTagPos + 1 ); // copy tag + else + { + highlightedText += text.mid( curPos, -1 ); // copy until end of string + break; + } + curPos = endTagPos + 1; + } + text = highlightedText; + } + + return beginning + text + ending; + } + +QString CardSideView::highlightKeyword( const QString aText, const FieldStyle& fieldStyle ) const + { + QString resText = aText; + if( !m_question.isEmpty() ) + resText.replace(QRegExp( "\\b(\\w*" + QRegExp::escape( m_question ) + "\\w*)\\b", + Qt::CaseInsensitive ), "[\\1]"); + QString spanBegin(""; + resText.replace('[', spanBegin); + resText.replace( ']', "" ); + return resText; + } + +QString CardSideView::getHighlighting(const FieldStyle& fieldStyle) const +{ + QString res; + if(fieldStyle.color != Qt::black) + res += QString("; color:%1").arg(fieldStyle.color.name()); + if(fieldStyle.font.bold()) + res += "; font-weight:bold"; + if(fieldStyle.font.italic()) + res += "; font-style:italic"; + return res; +} + +QSize CardSideView::sizeHint() const +{ + return QSize(300, 200); +} + +void CardSideView::updateNewIconPos() +{ + newIcon->move(rect().topRight() - QPoint(newIcon->width(), 0)); +} + +void CardSideView::showNewIcon(bool visible) +{ + newIcon->setVisible(visible); + updateNewIconPos(); +} + +void CardSideView::resizeEvent(QResizeEvent* /*event*/) +{ + updateNewIconPos(); +} diff --git a/src/study/CardSideView.h b/src/study/CardSideView.h new file mode 100644 index 0000000..c67ebe4 --- /dev/null +++ b/src/study/CardSideView.h @@ -0,0 +1,52 @@ +#ifndef CARDSIDEVIEW_H +#define CARDSIDEVIEW_H + +#include + +class CardPack; +class FieldStyle; +class Field; + +class CardSideView : public QLabel +{ + Q_OBJECT +public: + CardSideView( bool aMode = QstMode ); + void setPack( const CardPack* pack ); + void setQstAnsr( const QString aQuestion, const QStringList aAnswers ); + void setQuestion( const QString aQuestion ); + void setShowMode( bool aMode ); + QSize sizeHint() const; + QString getFormattedText() const; + void showNewIcon(bool visible); + +public: + static const bool QstMode = true; + static const bool AnsMode = false; + +public slots: + void setEnabled( bool aEnabled ); + +protected: + void resizeEvent(QResizeEvent* event); + +private: + void updateText(); + void setBackgroundVisible( bool aVisible ); + QString formattedField( const QString aField, const QString aStyle ) const; + QString highlightKeyword( const QString aText, const FieldStyle& fieldStyle ) const; + QString getHighlighting(const FieldStyle& fieldStyle) const; + QString getFormattedQuestion() const; + QString getFormattedAnswer() const; + QString getFormattedAnswerField(const Field* field, int index) const; + void updateNewIconPos(); + +private: + bool m_showMode; + const CardPack* cardPack; + QString m_question; + QStringList m_answers; + QLabel* newIcon; +}; + +#endif diff --git a/src/study/CardsStatusBar.cpp b/src/study/CardsStatusBar.cpp new file mode 100644 index 0000000..e29f9f8 --- /dev/null +++ b/src/study/CardsStatusBar.cpp @@ -0,0 +1,82 @@ +#include "CardsStatusBar.h" + +const QStringList CardsStatusBar::Colors = {"green", "#fffd7c", "white", "#bd8d71"}; + +CardsStatusBar::CardsStatusBar(QWidget* aParent) : + QWidget(aParent) +{ + setMinimumHeight(10); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); +} + +CardsStatusBar::~CardsStatusBar() +{ + delete painter; +} + +void CardsStatusBar::paintEvent(QPaintEvent* aEvent) +{ + QWidget::paintEvent( aEvent ); + if(values.isEmpty()) + return; + max = getMax(); + + painter = new QPainter(this); + painter->setRenderHint(QPainter::Antialiasing); + painter->save(); + + QPainterPath path; + path.addRoundedRect( rect(), Radius, Radius); + painter->setClipPath( path ); + + barRect = rect().adjusted(0, 0, 1, 0); + + sectionLeft = rect().left(); + for(int i = 0; i < values.size(); i++) + drawSection(i); + + painter->restore(); + + painter->setPen(QPen("#7c7c7c")); + painter->drawRoundedRect(rect(), Radius, Radius); +} + +int CardsStatusBar::getMax() const +{ + int max = 0; + foreach(int v, values) + max += v; + if(max == 0) + max = 1; + return max; +} + +void CardsStatusBar::drawSection(int index) +{ + QRectF sectionRect = getSectionRect(index); + painter->setPen( Qt::NoPen ); + painter->setBrush(getSectionGradient(index)); + painter->drawRect(sectionRect); + + painter->setPen(QPen(QBrush("#a7a7a7"), 0.5)); + painter->drawLine(sectionRect.topRight(), sectionRect.bottomRight()); + + sectionLeft += sectionRect.width(); +} + +QLinearGradient CardsStatusBar::getSectionGradient(int index) +{ + QLinearGradient grad(barRect.topLeft(), barRect.bottomLeft()); + grad.setColorAt( 0, Qt::white); + grad.setColorAt( 0.9, QColor(Colors[index]) ); + return grad; +} + +QRectF CardsStatusBar::getSectionRect(int index) +{ + QRectF sectionRect = barRect; + sectionRect.moveLeft(sectionLeft); + qreal sectionWidth = barRect.width() * values[index] / max; + sectionRect.setWidth(sectionWidth); + return sectionRect; +} diff --git a/src/study/CardsStatusBar.h b/src/study/CardsStatusBar.h new file mode 100644 index 0000000..10344f7 --- /dev/null +++ b/src/study/CardsStatusBar.h @@ -0,0 +1,37 @@ +#ifndef CARDSSTATUSBAR_H +#define CARDSSTATUSBAR_H + +#include + +class CardsStatusBar : public QWidget +{ + Q_OBJECT +public: + static const QStringList Colors; + +public: + CardsStatusBar(QWidget* aParent = 0); + ~CardsStatusBar(); + void setValues(const QList& values) { this->values = values; update(); } + +protected: + virtual void paintEvent(QPaintEvent* aEvent); + +private: + int getMax() const; + void drawSection(int index); + QLinearGradient getSectionGradient(int index); + QRectF getSectionRect(int index); + +private: + static const int Radius = 6; + +private: + QList values; + qreal max; + QRectF barRect; + QPainter* painter; + qreal sectionLeft; +}; + +#endif diff --git a/src/study/IStudyModel.cpp b/src/study/IStudyModel.cpp new file mode 100644 index 0000000..3194a71 --- /dev/null +++ b/src/study/IStudyModel.cpp @@ -0,0 +1,9 @@ +#include "IStudyModel.h" + +#include "../dictionary/CardPack.h" + +IStudyModel::IStudyModel( CardPack* aCardPack ): + cardPack(aCardPack), dictModel(NULL), curCardNum(-1) +{ +} + diff --git a/src/study/IStudyModel.h b/src/study/IStudyModel.h new file mode 100644 index 0000000..392ef01 --- /dev/null +++ b/src/study/IStudyModel.h @@ -0,0 +1,35 @@ +#ifndef ISTUDYMODEL_H +#define ISTUDYMODEL_H + +#include + +class Card; +class CardPack; +class DictTableModel; + +class IStudyModel: public QObject +{ +Q_OBJECT + +public: + IStudyModel( CardPack* aCardPack ); + virtual ~IStudyModel() {} + +public: + CardPack* getCardPack() { return cardPack; } + virtual Card* getCurCard() const = 0; + DictTableModel* getDictModel() const { return dictModel; } + + void setDictModel( DictTableModel* aModel ) { dictModel = aModel; } + +signals: + void nextCardSelected(); + void curCardUpdated(); + +protected: + CardPack* cardPack; + DictTableModel* dictModel; + int curCardNum; ///< Number of the current card in this session, base=0. +}; + +#endif diff --git a/src/study/IStudyWindow.cpp b/src/study/IStudyWindow.cpp new file mode 100644 index 0000000..f918936 --- /dev/null +++ b/src/study/IStudyWindow.cpp @@ -0,0 +1,293 @@ +#include "IStudyWindow.h" + +#include +#include + +#include "IStudyModel.h" +#include "CardSideView.h" +#include "CardEditDialog.h" +#include "../dictionary/Dictionary.h" +#include "../dictionary/CardPack.h" +#include "../main-view/MainWindow.h" + +const int IStudyWindow::AnsButtonPage = 0; +const int IStudyWindow::AnsLabelPage = 1; +const int IStudyWindow::CardPage = 0; +const int IStudyWindow::MessagePage = 1; + +IStudyWindow::IStudyWindow(IStudyModel* aModel , QString aStudyName, QWidget *aParent): + m_model( aModel ), curCard( NULL ), state(StateAnswerHidden), m_studyName( aStudyName ), + m_parentWidget( aParent ), m_cardEditDialog( NULL ) +{ + setMinimumWidth(MinWidth); + setMaximumWidth(MaxWidth); + setAttribute( Qt::WA_DeleteOnClose ); + connect( m_model, SIGNAL(nextCardSelected()), SLOT(showNextCard()) ); + connect( m_model, SIGNAL(curCardUpdated()), SLOT(updateCurCard()) ); +} + +IStudyWindow::~IStudyWindow() +{ + delete m_model; +} + +void IStudyWindow::OnDictionaryRemoved() +{ + close(); +} + +void IStudyWindow::createUI() +{ + centralStackedLt = new QStackedLayout; + centralStackedLt->addWidget(createWrapper(createCardView())); + centralStackedLt->addWidget(createWrapper(createMessageLayout())); + centralStackedLt->setCurrentIndex(CardPage); + connect(centralStackedLt, SIGNAL(currentChanged(int)), SLOT(updateToolBarVisibility(int))); + + QVBoxLayout* mainLayout = new QVBoxLayout; + mainLayout->addLayout(createUpperPanel()); + mainLayout->addLayout(centralStackedLt); + mainLayout->addLayout(createLowerPanel()); + setLayout(mainLayout); + + ReadSettings(); +} + +QWidget* IStudyWindow::createWrapper(QBoxLayout* layout) +{ + QWidget* widget = new QWidget; + widget->setLayout(layout); + return widget; +} + +QVBoxLayout* IStudyWindow::createMessageLayout() +{ + messageLabel = createMessageLabel(); + + QVBoxLayout* lt = new QVBoxLayout; + lt->setContentsMargins(QMargins()); + lt->addStretch(); + lt->addWidget(messageLabel); + lt->addStretch(); + lt->addWidget(createClosePackButton(), 0, Qt::AlignHCenter); + lt->addStretch(); + return lt; +} + +QLabel* IStudyWindow::createMessageLabel() +{ + QLabel* label = new QLabel; + label->setAutoFillBackground(true); + label->setPalette(QPalette("#fafafa")); + label->setWordWrap(true); + label->setFrameStyle(QFrame::StyledPanel); + label->setMinimumHeight(200); + return label; +} + +QPushButton* IStudyWindow::createClosePackButton() +{ + QPushButton* button = new QPushButton(tr("Close this pack")); + button->setFixedSize(BigButtonWidth, BigButtonHeight); + connect(button, SIGNAL(clicked()), SLOT(close())); + return button; +} + +QHBoxLayout* IStudyWindow::createUpperPanel() +{ + editCardBtn = createEditCardButton(); + deleteCardBtn = createDeleteCardButton(); + + QHBoxLayout* upperPanelLt = new QHBoxLayout; + upperPanelLt->addWidget(new QLabel(m_model->getCardPack()->id())); + upperPanelLt->addWidget(deleteCardBtn); + upperPanelLt->addWidget(editCardBtn); + + return upperPanelLt; +} + +QToolButton* IStudyWindow::createEditCardButton() +{ + QToolButton* button = new QToolButton( this ); + button->setIcon( QIcon(":/images/pencil.png") ); + button->setIconSize( QSize(ToolBarIconSize, ToolBarIconSize) ); + button->setShortcut( tr("E", "Shortcut for 'Edit card' button") ); + button->setToolTip( tr("Edit card")+" ("+button->shortcut().toString()+")" ); + button->setFixedSize( ToolBarButtonSize, ToolBarButtonSize ); + connect( button, SIGNAL(clicked()), SLOT(openCardEditDialog()) ); + return button; +} + +QToolButton* IStudyWindow::createDeleteCardButton() +{ + QToolButton* button = new QToolButton( this ); + button->setIcon( QIcon(":/images/red-cross.png") ); + button->setIconSize( QSize(ToolBarIconSize, ToolBarIconSize) ); + button->setShortcut( tr("D", "Shortcut for 'Delete card' button") ); + button->setToolTip( tr("Delete card")+" ("+button->shortcut().toString()+")" ); + button->setFixedSize( ToolBarButtonSize, ToolBarButtonSize ); + connect( button, SIGNAL(clicked()), SLOT(deleteCard()) ); + return button; +} + +QVBoxLayout* IStudyWindow::createCardView() +{ + questionLabel = new CardSideView( CardSideView::QstMode ); + questionLabel->setPack( m_model->getCardPack() ); + + answerStackedLt = new QStackedLayout; + answerStackedLt->addWidget(createWrapper(createAnswerButtonLayout())); + answerStackedLt->addWidget(createWrapper(createAnswerLayout())); + answerStackedLt->setCurrentIndex(AnsButtonPage); + + QVBoxLayout* cardViewLt = new QVBoxLayout; + cardViewLt->addWidget(questionLabel, 1); + cardViewLt->addLayout(answerStackedLt, 1); + + return cardViewLt; +} + +QBoxLayout* IStudyWindow::createAnswerButtonLayout() +{ + answerBtn = createAnswerButton(); + + QBoxLayout* lt = new QVBoxLayout; + lt->addStretch(); + if(getAnswerEdit()) + lt->addWidget(getAnswerEdit(), 0, Qt::AlignCenter); + lt->addWidget(answerBtn, 0, Qt::AlignCenter); + lt->addStretch(); + return lt; +} + +QPushButton* IStudyWindow::createAnswerButton() +{ + QPushButton* button = new QPushButton(QIcon(":/images/info.png"), tr("Show answer")); + button->setFixedSize(BigButtonWidth, BigButtonHeight); + button->setShortcut(Qt::Key_Return); + button->setToolTip(tr("Show answer") + ShortcutToStr(button) ); + connect(button, SIGNAL(clicked()), SLOT(showAnswer())); + return button; +} + +QBoxLayout* IStudyWindow::createAnswerLayout() +{ + answerLabel = new CardSideView( CardSideView::AnsMode ); + answerLabel->setPack( m_model->getCardPack() ); + + QBoxLayout* lt = new QVBoxLayout; + lt->setContentsMargins(QMargins()); + if(getUserAnswerLabel()) + lt->addWidget(getUserAnswerLabel(), 1); + lt->addWidget(answerLabel, 1); + return lt; +} + +const DictTableView* IStudyWindow::cardEditView() const + { + if( m_cardEditDialog ) + return m_cardEditDialog->cardEditView(); + else + return NULL; + } + +void IStudyWindow::openCardEditDialog() + { + if( m_cardEditDialog ) // already open + return; + Card* curCard = m_model->getCurCard(); + Q_ASSERT( curCard ); + MainWindow* mainWindow = dynamic_cast( m_parentWidget ); + if( !mainWindow ) + return; + m_cardEditDialog = new CardEditDialog( curCard, mainWindow, this ); + m_cardEditDialog->exec(); + delete m_cardEditDialog; + m_cardEditDialog = NULL; + Dictionary* dict = static_cast(m_model->getCardPack()->dictionary()); + dict->save(); + setWindowTitle(m_studyName + " - " + dict->shortName()); + } + +void IStudyWindow::deleteCard() + { + QString question = m_model->getCurCard()->getQuestion(); + QMessageBox::StandardButton pressedButton; + pressedButton = QMessageBox::question( this, tr("Delete card?"), tr("Delete card \"%1\"?").arg( question ), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes ); + if( pressedButton == QMessageBox::Yes ) + static_cast(m_model->getCardPack()->dictionary())-> + removeRecord( question ); + } + +void IStudyWindow::showNextCard() +{ + Q_ASSERT( m_model ); + if( curCard ) + disconnect( curCard, 0, this, 0 ); + curCard = m_model->getCurCard(); + if( curCard ) + { + connect( curCard, SIGNAL(answersChanged()), SLOT(updateCurCard()) ); + connect( curCard, SIGNAL(destroyed()), SLOT(invalidateCurCard()) ); + } + setWindowTitle( m_studyName + " - " + + static_cast(m_model->getCardPack()->dictionary())->shortName() ); + setStateForNextCard(); + processState(); +} + +void IStudyWindow::setStateForNextCard() + { + if( curCard ) + state = StateAnswerHidden; + else + state = StateNoCards; + } + +void IStudyWindow::showAnswer() + { + answerStackedLt->setCurrentIndex( AnsLabelPage ); + if( state == StateAnswerHidden ) + state = StateAnswerVisible; + else + return; + processState(); + } + +void IStudyWindow::updateCurCard() +{ + if( curCard ) + disconnect( curCard, 0, this, 0 ); + curCard = m_model->getCurCard(); + if( curCard ) + { + connect( curCard, SIGNAL(answersChanged()), SLOT(updateCurCard()) ); + connect( curCard, SIGNAL(destroyed()), SLOT(invalidateCurCard()) ); + } + setWindowTitle( m_studyName + " - " + + static_cast(m_model->getCardPack()->dictionary())->shortName() ); + int origState = state; + if( state == StateAnswerHidden || state == StateAnswerVisible ) + { + state = StateAnswerHidden; + processState(); + } + if( origState == StateAnswerVisible ) + { + state = StateAnswerVisible; + processState(); + } +} + +QString IStudyWindow::ShortcutToStr( QAbstractButton* aButton ) +{ + return " (" + aButton->shortcut().toString( QKeySequence::NativeText ) + ")"; +} + +void IStudyWindow::updateToolBarVisibility(int index) +{ + bool visible = (index == CardPage); + deleteCardBtn->setVisible(visible); + editCardBtn->setVisible(visible); +} diff --git a/src/study/IStudyWindow.h b/src/study/IStudyWindow.h new file mode 100644 index 0000000..1f0bd0d --- /dev/null +++ b/src/study/IStudyWindow.h @@ -0,0 +1,107 @@ +#ifndef ISTUDYWINDOW_H +#define ISTUDYWINDOW_H + +#include +#include + +#include "../main-view/AppModel.h" + +class IStudyModel; +class Card; +class Dictionary; +class QProgressBar; +class CardSideView; +class DictTableView; +class CardEditDialog; + +class IStudyWindow: public QWidget +{ + Q_OBJECT + +protected: + enum + { + StateAnswerHidden, + StateAnswerVisible, + StateNoCards, + StatesNum + }; + +public: + IStudyWindow(IStudyModel* aModel, QString aStudyName, QWidget* aParent); + virtual ~IStudyWindow(); + + AppModel::StudyType getStudyType() const { return studyType; } + IStudyModel* studyModel() const { return m_model; } + const DictTableView* cardEditView() const; + +protected: + void createUI(); + virtual QVBoxLayout* createLowerPanel() = 0; + virtual void setStateForNextCard(); + virtual void processState() = 0; + virtual void ReadSettings() = 0; + virtual QWidget* getAnswerEdit() { return NULL; } + virtual QWidget* getUserAnswerLabel() { return NULL; } + QString ShortcutToStr( QAbstractButton* aButton ); + bool bigScreen() { return QApplication::desktop()->screenGeometry().height()> 250; } + +private: + QVBoxLayout* createMessageLayout(); + QLabel* createMessageLabel(); + QWidget* createWrapper(QBoxLayout* layout); + QPushButton* createClosePackButton(); + QHBoxLayout* createUpperPanel(); + QToolButton* createEditCardButton(); + QToolButton* createDeleteCardButton(); + QVBoxLayout* createCardView(); + QBoxLayout* createAnswerButtonLayout(); + QPushButton* createAnswerButton(); + QBoxLayout* createAnswerLayout(); + +protected slots: + void OnDictionaryRemoved(); + void showNextCard(); ///< Entry point for showing a new card + void showAnswer(); ///< Entry point for showing the answer + void updateCurCard(); ///< Update the card after pack re-generation + void invalidateCurCard() { curCard = NULL; } + void openCardEditDialog(); + void deleteCard(); + void updateToolBarVisibility(int index); + +protected: + static const int AnsButtonPage; + static const int AnsLabelPage; + static const int CardPage; + static const int MessagePage; + + AppModel::StudyType studyType; + IStudyModel* m_model; + const Card* curCard; + int state; + + QString m_studyName; + QToolButton* editCardBtn; + QToolButton* deleteCardBtn; + + QStackedLayout* centralStackedLt; + QLabel* messageLabel; + + QStackedLayout* answerStackedLt; + QPushButton* answerBtn; + CardSideView* questionLabel; + CardSideView* answerLabel; + +private: + static const int MinWidth = 500; + static const int MaxWidth = 800; + static const int ToolBarButtonSize = 24; + static const int ToolBarIconSize = 16; + static const int BigButtonWidth = 160; + static const int BigButtonHeight = 50; + + QWidget* m_parentWidget; + CardEditDialog* m_cardEditDialog; +}; + +#endif diff --git a/src/study/NumberFrame.cpp b/src/study/NumberFrame.cpp new file mode 100644 index 0000000..1d2a2e1 --- /dev/null +++ b/src/study/NumberFrame.cpp @@ -0,0 +1,40 @@ +#include "NumberFrame.h" + +NumberFrame::NumberFrame(QWidget* parent): + QLabel(parent) +{ + init(); +} + +void NumberFrame::init() +{ + setMinimumSize(MinWidth, MinHeight); + setAlignment(Qt::AlignCenter); + setFrameShape(QFrame::NoFrame); +} + +void NumberFrame::setColor(const QColor& color) +{ + setPalette(QPalette(color)); +} + +void NumberFrame::setValue(int value) +{ + setMinimumWidth(value >= 100? MinWidth100: MinWidth); + setVisible(value > 0); + setText(QString::number(value)); +} + +void NumberFrame::paintEvent(QPaintEvent* event) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QPen pen("#7c7c7c"); + pen.setWidthF(0.5); + painter.setPen(pen); + painter.setBrush(QBrush(palette().color(QPalette::Button))); + painter.drawRoundedRect(rect().adjusted(1, 1, -1, -1), Radius, Radius); + + QLabel::paintEvent(event); +} diff --git a/src/study/NumberFrame.h b/src/study/NumberFrame.h new file mode 100644 index 0000000..44161ca --- /dev/null +++ b/src/study/NumberFrame.h @@ -0,0 +1,29 @@ +#ifndef NUMBER_FRAME_H +#define NUMBER_FRAME_H + +#include + +class NumberFrame: public QLabel +{ +public: + static const int MinWidth = 40; + static const int MinWidth100 = 50; + +public: + NumberFrame(QWidget* parent = 0); + void setColor(const QColor& color); + void setValue(int value); + +protected: + void paintEvent(QPaintEvent* event); + +private: + static const int Radius = 7; + static const int MinHeight = 16; + +private: + void init(); + +}; + +#endif diff --git a/src/study/SpacedRepetitionModel.cpp b/src/study/SpacedRepetitionModel.cpp new file mode 100644 index 0000000..e10299a --- /dev/null +++ b/src/study/SpacedRepetitionModel.cpp @@ -0,0 +1,390 @@ +#include "SpacedRepetitionModel.h" + +#include +#include +#include + +#include "../dictionary/Dictionary.h" +#include "../dictionary/Card.h" +#include "../dictionary/CardPack.h" +#include "../utils/IRandomGenerator.h" +#include "../utils/TimeProvider.h" + +SpacedRepetitionModel::SpacedRepetitionModel(CardPack* aCardPack, IRandomGenerator* random): + IStudyModel(aCardPack), + curCard(NULL), settings(StudySettings::inst()), random(random) + { + srand(time(NULL)); + curCardNum = 0; + connect(aCardPack, SIGNAL(cardsGenerated()), SLOT(updateStudyState())); + updateStudyState(); + } + +SpacedRepetitionModel::~SpacedRepetitionModel() + { + saveStudy(); + delete random; + } + +void SpacedRepetitionModel::saveStudy() + { + if(!cardPack) + return; + if(!cardPack->dictionary()) + return; + cardPack->dictionary()->saveStudy(); + } + +// Called after all cards are generated +void SpacedRepetitionModel::updateStudyState() + { + curCard = cardPack->getCard( cardPack->findLastReviewedCard() ); + curCardNum = 0; + if( !cardPack->curCardName().isEmpty() ) + { + prevCard = curCard; + curCard = cardPack->getCard( cardPack->curCardName() ); + if( curCard ) + { + answerTime.start(); + emit curCardUpdated(); + } + else + { + curCard = prevCard; + pickNextCardAndNotify(); + } + } + else + pickNextCardAndNotify(); + } + +void SpacedRepetitionModel::scheduleCard(int newGrade) + { + if(!curCard) + return; + saveStudyRecord(createNewStudyRecord(newGrade)); + curCardNum++; + pickNextCardAndNotify(); + } + +StudyRecord SpacedRepetitionModel::createNewStudyRecord(int newGrade) +{ + StudyRecord prevStudy = curCard->getStudyRecord(); + StudyRecord newStudy; + + newStudy.grade = newGrade; + newStudy.level = getNewLevel(prevStudy, newGrade); + newStudy.easiness = getNewEasiness(prevStudy, newGrade); + newStudy.interval = getNextInterval(prevStudy, newStudy); + newStudy.date = TimeProvider::get(); + newStudy.setRecallTime(curRecallTime / 1000.); + newStudy.setAnswerTime(answerTime.elapsed() / 1000.); + return newStudy; +} + +int SpacedRepetitionModel::getNewLevel(const StudyRecord& prevStudy, int newGrade) +{ + int level = prevStudy.level; + switch(newGrade) + { + case StudyRecord::Unknown: + case StudyRecord::Incorrect: + level = StudyRecord::ShortLearning; + break; + case StudyRecord::Difficult: + case StudyRecord::Good: + if(prevStudy.isOneDayOld()) + level = StudyRecord::Repeating; + else + level++; + break; + case StudyRecord::Easy: + level += 2; + break; + } + if(level > StudyRecord::LongLearning) + level = StudyRecord::Repeating; + return level; +} + +double SpacedRepetitionModel::getNewEasiness(const StudyRecord& prevStudy, + int newGrade) +{ + switch(prevStudy.level) + { + case StudyRecord::ShortLearning: + case StudyRecord::LongLearning: + if(prevStudy.isOneDayOld()) + return getChangeableEasiness(prevStudy, newGrade); + else + return prevStudy.easiness; + case StudyRecord::Repeating: + return getChangeableEasiness(prevStudy, newGrade); + default: + return prevStudy.easiness; + } +} + +double SpacedRepetitionModel::getChangeableEasiness(const StudyRecord& prevStudy, + int newGrade) const +{ + double eas = prevStudy.easiness; + switch(newGrade) + { + case StudyRecord::Difficult: + eas += settings->difficultDelta; + break; + case StudyRecord::Easy: + eas += settings->easyDelta; + break; + default: + return prevStudy.easiness; + } + return limitEasiness(eas); +} + +double SpacedRepetitionModel::limitEasiness(double eas) const +{ + if(eas < settings->minEasiness) + eas = settings->minEasiness; + else if(eas > settings->maxEasiness) + eas = settings->maxEasiness; + return eas; +} + +double SpacedRepetitionModel::getNextInterval(const StudyRecord& prevStudy, + const StudyRecord& newStudy) +{ + switch(newStudy.level) + { + case StudyRecord::ShortLearning: + if(newStudy.grade == StudyRecord::Incorrect) + return settings->incorrectInterval; + else + return settings->unknownInterval; + case StudyRecord::LongLearning: + return settings->learningInterval; + case StudyRecord::Repeating: + return getNextRepeatingInterval(prevStudy, newStudy); + default: + return 0; + } +} + +double SpacedRepetitionModel::getNextRepeatingInterval(const StudyRecord& prevStudy, + const StudyRecord& newStudy) +{ + switch(prevStudy.level) + { + case StudyRecord::ShortLearning: + return getNextRepeatingIntervalForShortLearning(prevStudy, newStudy); + case StudyRecord::LongLearning: + return getNextRepeatingIntervalForLongLearning(prevStudy, newStudy); + case StudyRecord::Repeating: + return getIncreasedInterval(prevStudy.interval, newStudy.easiness); + default: + return settings->nextDayInterval; + } +} + +double SpacedRepetitionModel::getIncreasedInterval(double prevInterval, + double newEasiness) +{ + double interval = prevInterval * newEasiness; + interval += interval * + settings->schedRandomness * random->getInRange_11(); + return interval; +} + +double SpacedRepetitionModel::getNextRepeatingIntervalForShortLearning( + const StudyRecord& prevStudy, + const StudyRecord& newStudy) +{ + if(prevStudy.isOneDayOld()) + return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness); + else + return settings->nextDayInterval; +} + +double SpacedRepetitionModel::getNextRepeatingIntervalForLongLearning( + const StudyRecord& prevStudy, + const StudyRecord& newStudy) +{ + if(prevStudy.isOneDayOld()) + return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness); + else if(newStudy.grade == StudyRecord::Easy) + return settings->twoDaysInterval; + else + return settings->nextDayInterval; +} + +void SpacedRepetitionModel::saveStudyRecord(const StudyRecord& newStudy) +{ + cardPack->addStudyRecord(curCard->getQuestion(), newStudy); +} + +QList SpacedRepetitionModel::getAvailableGrades() const +{ + if(!curCard) + return {}; + StudyRecord study = curCard->getStudyRecord(); + switch(study.level) + { + case StudyRecord::New: + return {4, 5}; + case StudyRecord::ShortLearning: + case StudyRecord::LongLearning: + if(study.isOneDayOld()) + return {1, 2, 3, 4, 5}; + else + return {1, 2, 4, 5}; + case StudyRecord::Repeating: + return {1, 2, 3, 4, 5}; + default: + return {}; + } +} + +bool SpacedRepetitionModel::isNew() const +{ + return curCard->getStudyRecord().level == StudyRecord::New; +} + +void SpacedRepetitionModel::pickNextCardAndNotify() + { + answerTime.start(); + pickNextCard(); + if(curCard) + cardPack->setCurCard(curCard->getQuestion()); + else + cardPack->setCurCard(""); + // Notify the study window to show the selected card. + emit nextCardSelected(); + } + +void SpacedRepetitionModel::pickNextCard() +{ + prevCard = curCard; + curCard = NULL; + pickActiveCard() || + pickNewCard() || + pickLearningCard(); +} + +bool SpacedRepetitionModel::mustRandomPickScheduledCard() const +{ + return random->getInRange_01() > settings->newCardsShare; +} + +bool SpacedRepetitionModel::reachedNewCardsDayLimit() const +{ + return cardPack->getTodayNewCardsNum() >= settings->newCardsDayLimit; +} + +bool SpacedRepetitionModel::pickActiveCard() +{ + QStringList activeCards = cardPack->getActiveCards(); + if(activeCards.isEmpty()) + return false; + if(pickPriorityActiveCard()) + return true; + if(!mustPickScheduledCard()) + return false; + curCard = cardPack->getCard(getRandomStr(activeCards)); + return true; +} + +bool SpacedRepetitionModel::pickPriorityActiveCard() +{ + QStringList priorityCards = cardPack->getPriorityActiveCards(); + if(priorityCards.isEmpty()) + return false; + QStringList smallestIntervals = cardPack->getSmallestIntervalCards(priorityCards); + curCard = cardPack->getCard(getRandomStr(smallestIntervals)); + return true; +} + +bool SpacedRepetitionModel::mustPickScheduledCard() +{ + bool noNewCards = cardPack->getNewCards().isEmpty(); + if(noNewCards || reachedNewCardsDayLimit() || + tooManyScheduledCards()) + return true; + else + return mustRandomPickScheduledCard(); +} + +bool SpacedRepetitionModel::tooManyScheduledCards() const +{ + return cardPack->countScheduledForTodayCards() >= settings->limitForAddingNewCards; +} + +bool SpacedRepetitionModel::pickNewCard() + { + if(reachedNewCardsDayLimit()) + return false; + QStringList newCards = cardPack->getNewCards(); + if(newCards.isEmpty()) + return false; + QString cardName; + if(settings->showRandomly) + cardName = getRandomStr(newCards); + else + cardName = newCards.first(); + curCard = cardPack->getCard(cardName); + return true; + } + +bool SpacedRepetitionModel::pickLearningCard() +{ + QStringList learningCards = cardPack->getLearningCards(); + if(learningCards.isEmpty()) + return false; + QStringList smallestIntervals = cardPack->getSmallestIntervalCards(learningCards); + curCard = cardPack->getCard(getRandomStr(smallestIntervals)); + return true; +} + +QString SpacedRepetitionModel::getRandomStr(const QStringList& list) const +{ + return list.at(random->getRand(list.size())); +} + +/// New cards inside the reviewed ones still today +int SpacedRepetitionModel::estimatedNewReviewedCardsToday() const + { + if(tooManyScheduledCards()) + return 0; + int scheduledToday = cardPack->countScheduledForTodayCards(); + int newRev = 0; + if( scheduledToday > 0 ) + { + float newShare = settings->newCardsShare; + newRev = qRound( scheduledToday * newShare ); + } + else + return 0; + + // Check for remained new cards in pack + int newCardsNum = cardPack->getNewCards().size(); + if( newRev > newCardsNum ) + newRev = newCardsNum; + + // Check for new cards day limit + int newCardsDayLimit = settings->newCardsDayLimit; + int todayReviewedNewCards = cardPack->getTodayNewCardsNum(); + int remainedNewCardsLimit = newCardsDayLimit - todayReviewedNewCards; + if( newRev > remainedNewCardsLimit ) + newRev = remainedNewCardsLimit; + + return newRev; + } + +/** Calculates number of candidate cards to be shown in the current session. + */ +int SpacedRepetitionModel::countTodayRemainingCards() const + { + int timeTriggered = cardPack->getActiveCards().size(); + return timeTriggered + estimatedNewReviewedCardsToday(); + } diff --git a/src/study/SpacedRepetitionModel.h b/src/study/SpacedRepetitionModel.h new file mode 100644 index 0000000..d39a1b8 --- /dev/null +++ b/src/study/SpacedRepetitionModel.h @@ -0,0 +1,79 @@ +#ifndef SPACEDREPETITIONMODEL_H +#define SPACEDREPETITIONMODEL_H + +#include "IStudyModel.h" +#include "StudyRecord.h" +#include "StudySettings.h" + +#include +#include + +class CardPack; +class IRandomGenerator; + +class SpacedRepetitionModel: public IStudyModel +{ + Q_OBJECT + +public: + static const int NewCardsDayLimit = 10; + +public: + SpacedRepetitionModel(CardPack* aCardPack, IRandomGenerator* random); + ~SpacedRepetitionModel(); + +public: + Card* getCurCard() const { return curCard; } + QList getAvailableGrades() const; + bool isNew() const; + void setRecallTime( int aTime ) { curRecallTime = aTime; } + int estimatedNewReviewedCardsToday() const; + int countTodayRemainingCards() const; + +public slots: + void scheduleCard(int newGrade); + +protected: + void pickNextCardAndNotify(); + +private: + StudyRecord createNewStudyRecord(int newGrade); + int getNewLevel(const StudyRecord& prevStudy, int newGrade); + double getNewEasiness(const StudyRecord& prevStudy, int newGrade); + double getChangeableEasiness(const StudyRecord& prevStudy, int newGrade) const; + double limitEasiness(double eas) const; + double getNextInterval(const StudyRecord& prevStudy, + const StudyRecord& newStudy); + double getNextRepeatingInterval(const StudyRecord& prevStudy, + const StudyRecord& newStudy); + double getIncreasedInterval(double prevInterval, double newEasiness); + double getNextRepeatingIntervalForShortLearning( + const StudyRecord& prevStudy, const StudyRecord& newStudy); + double getNextRepeatingIntervalForLongLearning( + const StudyRecord& prevStudy, const StudyRecord& newStudy); + void saveStudyRecord(const StudyRecord& newStudy); + void pickNextCard(); + bool pickNewCard(); + QString getRandomStr(const QStringList& list) const; + bool reachedNewCardsDayLimit() const; + bool mustPickScheduledCard(); + bool tooManyScheduledCards() const; + bool mustRandomPickScheduledCard() const; + bool pickActiveCard(); + bool pickPriorityActiveCard(); + bool pickLearningCard(); + void saveStudy(); + +private slots: + void updateStudyState(); + +private: + Card* curCard; ///< The card selected for repetition + Card* prevCard; ///< Previous reviewed card. Found in the study history. + int curRecallTime; ///< Recall time of the current card, ms + QTime answerTime; ///< Full answer time of the current card: recall + evaluation, ms + StudySettings* settings; + IRandomGenerator* random; +}; + +#endif diff --git a/src/study/SpacedRepetitionWindow.cpp b/src/study/SpacedRepetitionWindow.cpp new file mode 100644 index 0000000..977b1ec --- /dev/null +++ b/src/study/SpacedRepetitionWindow.cpp @@ -0,0 +1,380 @@ +#include "SpacedRepetitionWindow.h" +#include "CardsStatusBar.h" +#include "NumberFrame.h" +#include "WarningPanel.h" +#include "../dictionary/Dictionary.h" +#include "../dictionary/Card.h" +#include "../dictionary/CardPack.h" + +SpacedRepetitionWindow::SpacedRepetitionWindow( SpacedRepetitionModel* aModel, QWidget* aParent ): + IStudyWindow( aModel, tr("Spaced repetition"), aParent ), + exactAnswerLabel(NULL), exactAnswerEdit(NULL) +{ + usesExactAnswer = m_model->getCardPack()->getUsesExactAnswer(); + studyType = AppModel::SpacedRepetition; + if(usesExactAnswer) + createExactAnswerWidget(); + createUI(); // Can't call from parent constructor + setWindowIcon( QIcon(":/images/spaced-rep.png") ); + dayCardsLimitShown = false; + showNextCard(); +} + +SpacedRepetitionWindow::~SpacedRepetitionWindow() +{ + WriteSettings(); +} + +void SpacedRepetitionWindow::createExactAnswerWidget() +{ + exactAnswerEdit = new QLineEdit; + + exactAnswerLabel = new CardSideView; + exactAnswerLabel->setMaximumHeight(40); + exactAnswerLabel->setShowMode(CardSideView::AnsMode); + exactAnswerLabel->setPack(m_model->getCardPack()); +} + +QVBoxLayout* SpacedRepetitionWindow::createLowerPanel() +{ + warningPanel = new WarningPanel; + + controlLt = new QVBoxLayout; + controlLt->addWidget(warningPanel); + controlLt->addLayout(createProgressLayout()); + controlLt->addLayout(new QVBoxLayout); // Grades layout + + createGradeButtons(); + + return controlLt; +} + +QBoxLayout* SpacedRepetitionWindow::createProgressLayout() +{ + QVBoxLayout* lt = new QVBoxLayout; + lt->setSpacing(3); + lt->addLayout(createStatsLayout()); + lt->addLayout(createProgressBarLayout()); + return lt; +} + +QBoxLayout* SpacedRepetitionWindow::createStatsLayout() +{ + todayNewLabel = new NumberFrame; + todayNewLabel->setToolTip(tr("Today learned new cards")); + todayNewLabel->setColor("#c0d6ff"); + + learningCardsLabel = new NumberFrame; + learningCardsLabel->setToolTip(tr("Scheduled learning reviews:\n" + "new cards must be repeated today to learn")); + learningCardsLabel->setColor(CardsStatusBar::Colors[1]); + + timeToNextLearningLabel = new QLabel; + timeToNextLearningLabel->setToolTip(tr("Time left to the next learning review")); + + scheduledCardsLabel = new NumberFrame; + scheduledCardsLabel->setToolTip(tr("Scheduled cards for today")); + scheduledCardsLabel->setColor(CardsStatusBar::Colors[2]); + + scheduledNewLabel = new NumberFrame; + scheduledNewLabel->setToolTip(tr("New scheduled cards for today:\n" + "new cards that will be shown between the scheduled ones")); + scheduledNewLabel->setColor("#bd8d71"); + + QHBoxLayout* lt = new QHBoxLayout; + lt->setSpacing(5); + lt->addWidget(todayNewLabel); + lt->addStretch(); + lt->addWidget(learningCardsLabel); + lt->addWidget(timeToNextLearningLabel); + lt->addWidget(scheduledCardsLabel); + lt->addWidget(scheduledNewLabel); + return lt; +} + +QBoxLayout* SpacedRepetitionWindow::createProgressBarLayout() +{ + progressLabel = new QLabel; + progressLabel->setToolTip(getProgressBarTooltip()); + + coloredProgressBar = new CardsStatusBar; + coloredProgressBar->setToolTip(progressLabel->toolTip()); + + QHBoxLayout* lt = new QHBoxLayout; + lt->setSpacing(5); + lt->addWidget(progressLabel); + lt->addWidget(coloredProgressBar); + return lt; +} + +QString SpacedRepetitionWindow::getProgressBarTooltip() +{ + QString boxSpace; + for(int i = 0; i < 6; i++) + boxSpace += " "; + QString colorBoxPattern = "

" + + boxSpace + "  "; + QString pEnd = "

"; + + QStringList legendLines = { + tr("Reviewed cards"), + tr("Learning reviews"), + tr("Scheduled cards"), + tr("New cards for today")}; + QString legend; + for(int i = 0; i < 4; i++) + legend += colorBoxPattern.arg(CardsStatusBar::Colors[i]) + legendLines[i] + pEnd; + return tr("Progress of reviews scheduled for today:") + legend; +} + +void SpacedRepetitionWindow::createGradeButtons() +{ + cardGradedSM = new QSignalMapper(this); + connect( cardGradedSM, SIGNAL(mapped(int)), + qobject_cast(m_model), SLOT(scheduleCard(int)) ); + + gradeBtns[0] = createGradeButton(0, "question.png", tr("Unknown"), + tr("Completely forgotten card, couldn't recall the answer.")); + gradeBtns[1] = createGradeButton(1, "red-stop.png", tr("Incorrect"), + tr("The answer is incorrect.")); + gradeBtns[2] = createGradeButton(2, "blue-triangle-down.png", tr("Difficult"), + tr("It's difficult to recall the answer. The last interval was too long.")); + gradeBtns[3] = createGradeButton(3, "green-tick.png", tr("Good"), + tr("The answer is recalled in couple of seconds. The last interval was good enough.")); + gradeBtns[4] = createGradeButton(4, "green-triangle-up.png", tr("Easy"), + tr("The card is too easy, and recalled without any effort. The last interval was too short.")); + goodBtn = gradeBtns[StudyRecord::Good - 1]; +} + +QPushButton* SpacedRepetitionWindow::createGradeButton(int i, const QString& iconName, + const QString& label, const QString& toolTip) +{ + QPushButton* btn = new QPushButton; + btn->setIcon(QIcon(":/images/" + iconName)); + btn->setText(QString("%1 %2").arg(i + 1).arg(label)); + btn->setToolTip("

" + toolTip); + btn->setShortcut( QString::number(i + 1) ); + btn->setMinimumWidth(GradeButtonMinWidth); + +#if defined(Q_OS_WIN) + QFont font = btn->font(); + font.setPointSize(12); + font.setFamily("Calibri"); + btn->setFont(font); +#endif + + cardGradedSM->setMapping(btn, i + 1); + connect(btn, SIGNAL(clicked()), cardGradedSM, SLOT(map())); + return btn; +} + +void SpacedRepetitionWindow::processState() +{ + setEnabledGradeButtons(); + switch(state) + { + case StateAnswerHidden: + displayQuestion(); + break; + case StateAnswerVisible: + displayAnswer(); + break; + case StateNoCards: + updateCardsProgress(); + displayNoRemainedCards(); + break; + } +} + +void SpacedRepetitionWindow::setEnabledGradeButtons() +{ + for(int i = 0; i < StudyRecord::GradesNum; i++) + gradeBtns[i]->setEnabled(state == StateAnswerVisible); +} + +void SpacedRepetitionWindow::displayQuestion() +{ + questionLabel->setQuestion( m_model->getCurCard()->getQuestion() ); + answerStackedLt->setCurrentIndex( AnsButtonPage ); + updateCardsProgress(); + layoutGradeButtons(); + focusAnswerWidget(); + m_answerTime.start(); + processIsNewCard(); +} + +void SpacedRepetitionWindow::layoutGradeButtons() +{ + QHBoxLayout* higherLt = new QHBoxLayout; + higherLt->addStretch(); + putGradeButtonsIntoLayouts(higherLt); + higherLt->addStretch(); + replaceGradesLayout(higherLt); + setGoodButtonText(); +} + +void SpacedRepetitionWindow::putGradeButtonsIntoLayouts(QBoxLayout* higherLt) +{ + QList visibleGrades = static_cast(m_model) + ->getAvailableGrades(); + for(int i = 0; i < StudyRecord::GradesNum; i++) + { + if(!visibleGrades.contains(i + 1)) + { + gradeBtns[i]->setParent(NULL); + continue; + } + higherLt->addWidget(gradeBtns[i]); + } +} + +void SpacedRepetitionWindow::replaceGradesLayout(QBoxLayout* higherLt) +{ + const int GradesLtIndex = 2; + delete controlLt->takeAt(GradesLtIndex); + controlLt->insertLayout(GradesLtIndex, higherLt); +} + +void SpacedRepetitionWindow::setGoodButtonText() +{ + QList visibleGrades = static_cast(m_model) + ->getAvailableGrades(); + QString goodLabel = visibleGrades.size() == StudyRecord::GradesNum? + tr("Good") : tr("OK"); + goodBtn->setText(QString("%1 %2").arg(StudyRecord::Good).arg(goodLabel)); + goodBtn->setShortcut(QString::number(StudyRecord::Good)); +} + +void SpacedRepetitionWindow::focusAnswerWidget() +{ + if(usesExactAnswer) + { + exactAnswerEdit->clear(); + exactAnswerEdit->setFocus(); + } + else + answerBtn->setFocus(); +} + +void SpacedRepetitionWindow::processIsNewCard() +{ + questionLabel->showNewIcon(static_cast(m_model)->isNew()); +} + +void SpacedRepetitionWindow::displayAnswer() +{ + static_cast(m_model)->setRecallTime(m_answerTime.elapsed()); + Card* card = m_model->getCurCard(); + QStringList correctAnswers = card->getAnswers(); + answerStackedLt->setCurrentIndex(AnsLabelPage); + if(usesExactAnswer) + showExactAnswer(correctAnswers); + answerLabel->setQstAnsr(card->getQuestion(), correctAnswers); + goodBtn->setFocus(); +} + +void SpacedRepetitionWindow::showExactAnswer(const QStringList& correctAnswers) +{ + if(static_cast(m_model)->isNew()) + { + exactAnswerLabel->hide(); + return; + } + else + exactAnswerLabel->show(); + bool isCorrect = correctAnswers.first() == exactAnswerEdit->text().trimmed(); + QString beginning(""; + QString answer = beginning + exactAnswerEdit->text() + ""; + exactAnswerLabel->setQstAnsr("", {answer}); +} + +void SpacedRepetitionWindow::updateCardsProgress() + { + CardPack* pack = m_model->getCardPack(); + int todayReviewed = pack->getTodayReviewedCardsNum(); + int todayNewReviewed = pack->getTodayNewCardsNum(); + int activeRepeating = pack->getActiveRepeatingCardsNum(); + int learningReviews = pack->getLearningReviewsNum(); + int timeToNextLearning = pack->getTimeToNextLearning(); + int scheduledNew = static_cast(m_model)-> + estimatedNewReviewedCardsToday(); + int allTodaysCards = todayReviewed + learningReviews + + activeRepeating + scheduledNew; + + todayNewLabel->setVisible(todayNewReviewed > 0); + todayNewLabel->setValue(todayNewReviewed); + learningCardsLabel->setValue(learningReviews); + + int minsToNextLearning = timeToNextLearning / 60; + timeToNextLearningLabel->setVisible(minsToNextLearning > 0); + timeToNextLearningLabel->setText(tr("(%1 min)"). + arg(minsToNextLearning)); + + scheduledCardsLabel->setValue(activeRepeating); + scheduledNewLabel->setValue(scheduledNew); + + progressLabel->setText(QString("%1/%2"). + arg(todayReviewed).arg(allTodaysCards)); + coloredProgressBar->setValues({todayReviewed, learningReviews, + activeRepeating, scheduledNew}); + + showLimitWarnings(); +} + +void SpacedRepetitionWindow::showLimitWarnings() +{ + if(warningPanel->isVisible()) + return; + CardPack* pack = m_model->getCardPack(); + if(dayLimitReached(pack->dictionary()->countTodaysAllCards())) + { + warningPanel->setText(tr("Day cards limit is reached: %1 cards.\n" + "It is recommended to stop studying this dictionary.") + .arg(StudySettings::inst()->cardsDayLimit)); + warningPanel->show(); + dayCardsLimitShown = true; + } +} + +bool SpacedRepetitionWindow::dayLimitReached(int todayReviewed) +{ + return todayReviewed >= StudySettings::inst()->cardsDayLimit && + !dayCardsLimitShown; +} + +void SpacedRepetitionWindow::displayNoRemainedCards() +{ + centralStackedLt->setCurrentIndex(MessagePage); + messageLabel->setText( + QString("

") + + tr("All cards are reviewed") + + "
" + "

" + + tr("You can go to the next pack or dictionary, or open the Word drill.") + + "

"); +} + +void SpacedRepetitionWindow::ReadSettings() +{ + QSettings settings; + move( settings.value("spacedrep-pos", QPoint(PosX, PosY)).toPoint() ); + resize( settings.value("spacedrep-size", QSize(Width, Height)).toSize() ); +} + +void SpacedRepetitionWindow::WriteSettings() +{ + QSettings settings; + settings.setValue("spacedrep-pos", pos()); + settings.setValue("spacedrep-size", size()); +} + +QWidget* SpacedRepetitionWindow::getAnswerEdit() +{ + return exactAnswerEdit; +} + +QWidget* SpacedRepetitionWindow::getUserAnswerLabel() +{ + return exactAnswerLabel; +} diff --git a/src/study/SpacedRepetitionWindow.h b/src/study/SpacedRepetitionWindow.h new file mode 100644 index 0000000..942b7f2 --- /dev/null +++ b/src/study/SpacedRepetitionWindow.h @@ -0,0 +1,89 @@ +#ifndef SPACED_REPETITION_WINDOW_H +#define SPACED_REPETITION_WINDOW_H + +#include +#include + +#include "IStudyWindow.h" +#include "StudyRecord.h" +#include "StudySettings.h" +#include "SpacedRepetitionModel.h" +#include "CardSideView.h" + +class CardsStatusBar; +class NumberFrame; +class WarningPanel; + +class SpacedRepetitionWindow: public IStudyWindow +{ + Q_OBJECT + +public: + SpacedRepetitionWindow( SpacedRepetitionModel* aModel, QWidget* aParent ); + ~SpacedRepetitionWindow(); + +protected: + QVBoxLayout* createLowerPanel(); + void processState(); + void ReadSettings(); + void WriteSettings(); + QWidget* getAnswerEdit(); + QWidget* getUserAnswerLabel(); + +private: + static QString getProgressBarTooltip(); + +private: + QBoxLayout* createProgressLayout(); + QBoxLayout* createStatsLayout(); + QBoxLayout* createProgressBarLayout(); + void createGradeButtons(); + QPushButton* createGradeButton(int i, const QString& iconName, + const QString& label, const QString& toolTip); + void setEnabledGradeButtons(); + void layoutGradeButtons(); + void putGradeButtonsIntoLayouts(QBoxLayout* higherLt); + void replaceGradesLayout(QBoxLayout* higherLt); + void setGoodButtonText(); + void updateCardsProgress(); + void displayAnswer(); + void displayNoRemainedCards(); + void showLimitWarnings(); + bool dayLimitReached(int todayReviewed); + void createExactAnswerWidget(); + void showExactAnswer(const QStringList& correctAnswers); + void focusAnswerWidget(); + void processIsNewCard(); + +private slots: + void displayQuestion(); + +private: + static const int PosX = 200; + static const int PosY = 200; + static const int Width = 600; + static const int Height = 430; + static const int GradeButtonMinWidth = 100; + +private: + bool dayCardsLimitShown; + + WarningPanel* warningPanel; + QLabel* progressLabel; + NumberFrame* todayNewLabel; + NumberFrame* learningCardsLabel; + QLabel* timeToNextLearningLabel; + NumberFrame* scheduledCardsLabel; + NumberFrame* scheduledNewLabel; + QVBoxLayout* controlLt; + CardsStatusBar* coloredProgressBar; + QPushButton* gradeBtns[StudyRecord::GradesNum]; + QPushButton* goodBtn; + QSignalMapper* cardGradedSM; + bool usesExactAnswer; + CardSideView* exactAnswerLabel; + QLineEdit* exactAnswerEdit; + QTime m_answerTime; +}; + +#endif diff --git a/src/study/StudyFileReader.cpp b/src/study/StudyFileReader.cpp new file mode 100644 index 0000000..ecc4dbf --- /dev/null +++ b/src/study/StudyFileReader.cpp @@ -0,0 +1,238 @@ +#include "StudyFileReader.h" +#include "../dictionary/Dictionary.h" +#include "../version.h" +#include "../dictionary/CardPack.h" +#include "../dictionary/DicRecord.h" + +#include + +const QString StudyFileReader::MinSupportedStudyVersion = "1.0"; + +StudyFileReader::StudyFileReader( Dictionary* aDict ): + m_dict( aDict ), m_cardPack( NULL ) + { + settings = StudySettings::inst(); + } + +bool StudyFileReader::read( QIODevice* aDevice ) + { + setDevice( aDevice ); + while( !atEnd() ) + { + readNext(); + if( isStartElement() ) + { + if( name() == "study" ) + { + readStudy(); + return !error(); // readStudy() processes all situations + } + else + raiseError( Dictionary::tr("The file is not a study file.") ); + } + } + return !error(); + } + +void StudyFileReader::readStudy() + { + Q_ASSERT( isStartElement() && name() == "study" ); + + m_studyVersion = attributes().value( "version" ).toString(); + if(m_studyVersion >= MinSupportedStudyVersion) + readStudyCurrentVersion(); + else + QMessageBox::warning( NULL, Dictionary::tr("Unsupported format"), + Dictionary::tr("The study file uses unsupported format %1.\n" + "The minimum supported version is %2" ) + .arg( m_studyVersion ) + .arg( MinSupportedStudyVersion ) ); + } + +void StudyFileReader::readUnknownElement() + { + Q_ASSERT( isStartElement() ); + while( !atEnd() ) + { + readNext(); + if( isEndElement() ) + break; + if( isStartElement() ) + readUnknownElement(); + } + } + +void StudyFileReader::readStudyCurrentVersion() + { + Q_ASSERT( isStartElement() && name() == "study" ); + + while( !atEnd() ) + { + readNext(); + if( isEndElement() ) + break; + if( isStartElement() ) + { + if( name() == "pack" ) + readPack(); + else + readUnknownElement(); + } + } + if(m_studyVersion != STUDY_VERSION) + m_dict->setStudyModified(); + } + +void StudyFileReader::readPack() + { + Q_ASSERT( isStartElement() && name() == "pack" ); + + QString id = attributes().value( "id" ).toString(); + m_cardPack = m_dict->cardPack( id ); + if( !m_cardPack ) + { + skipCurrentElement(); + return; + } + m_cardPack->setReadingStudyFile(true); + + while( !atEnd() ) + { + readNext(); + if( isEndElement() ) + break; + if( isStartElement() ) + { + if( name() == "cur-card" ) + { + Q_ASSERT( m_cardPack->curCardName().isEmpty() ); + QString curCard = attributes().value( "id" ).toString(); + if( !curCard.isEmpty() ) + m_cardPack->setCurCard( curCard ); + readNext(); // read end of 'cur-card' element + Q_ASSERT( isEndElement() ); + } + else if( name() == "c" ) + readC(); + else + readUnknownElement(); + } + } + + m_cardPack->setReadingStudyFile(false); + } + +void StudyFileReader::readC() + { + Q_ASSERT( isStartElement() && name() == "c" && m_cardPack ); + + m_cardId = attributes().value( "id" ).toString(); + + while( !atEnd() ) + { + readNext(); + if( isEndElement() ) + break; + if( isStartElement() ) + { + if( name() == "r" ) + readR(); + else + readUnknownElement(); + } + } + } + +void StudyFileReader::readR() + { + Q_ASSERT( isStartElement() && name() == "r" ); + + StudyRecord study; + bool ok; // for number conversions + + QString date = attributes().value( "d" ).toString(); + study.date = QDateTime::fromString( date, Qt::ISODate ); + + QString grade = attributes().value( "g" ).toString(); + study.grade = grade.toInt( &ok ); + if(m_studyVersion < "1.4" && study.grade < 3) + study.grade++; + + QString easiness = attributes().value( "e" ).toString(); + study.easiness = easiness.toFloat( &ok ); + + QString time; + time = attributes().value( "t" ).toString(); // obsolete parameter + if(!time.isEmpty()) + { + study.recallTime = time.toFloat( &ok ); + Q_ASSERT( ok ); + } + + time = attributes().value( "rt" ).toString(); + if(!time.isEmpty()) + { + study.recallTime = time.toFloat( &ok ); + if( study.recallTime > StudyRecord::MaxAnswerTime ) + study.recallTime = StudyRecord::MaxAnswerTime; + Q_ASSERT( ok ); + } + + time = attributes().value( "at" ).toString(); + if(!time.isEmpty()) + { + study.answerTime = time.toFloat( &ok ); + if( study.answerTime > StudyRecord::MaxAnswerTime ) + study.answerTime = StudyRecord::MaxAnswerTime; + Q_ASSERT( ok ); + } + + QString interval = attributes().value( "i" ).toString(); + QString level = attributes().value( "l" ).toString(); + if(!interval.isEmpty() && interval != "0") + { + study.interval = interval.toFloat(); + if(!level.isEmpty()) + study.level = level.toInt(); + else + study.level = StudyRecord::Repeating; + if(m_studyVersion == "1.4") + fixupIncorrectGradeIn1_4(study); + } + else // delayed card + { + QString cardsStr = attributes().value( "c" ).toString(); + if( !cardsStr.isEmpty() ) + { + int cardInterval = cardsStr.toInt(); + if(cardInterval <= 7) + study.interval = settings->unknownInterval; + else + study.interval = settings->incorrectInterval; + } + else + study.interval = 0; + study.level = StudyRecord::ShortLearning; + } + + m_cardPack->addStudyRecord( m_cardId, study ); + + readElementText(); // read until element end + } + +void StudyFileReader::fixupIncorrectGradeIn1_4(StudyRecord& study) +{ + if(!(study.level == StudyRecord::ShortLearning && + study.grade <= 3)) + return; + if(equalDouble(study.interval, settings->unknownInterval)) + study.grade = StudyRecord::Unknown; + else if(equalDouble(study.interval, settings->incorrectInterval)) + study.grade = StudyRecord::Incorrect; +} + +bool StudyFileReader::equalDouble(double a, double b) +{ + static const double Dif = 0.0001; + return fabs(a - b) < Dif; +} diff --git a/src/study/StudyFileReader.h b/src/study/StudyFileReader.h new file mode 100644 index 0000000..2466fe3 --- /dev/null +++ b/src/study/StudyFileReader.h @@ -0,0 +1,44 @@ +#ifndef STUDYFILEREADER_H +#define STUDYFILEREADER_H + +#include +#include +#include + +#include "StudySettings.h" +#include "StudyRecord.h" + +class Dictionary; +class CardPack; +class DicRecord; + +class StudyFileReader : public QXmlStreamReader +{ +public: + StudyFileReader( Dictionary* aDict ); + bool read( QIODevice* aDevice ); + +private: + static const QString MinSupportedStudyVersion; + +private: + void readStudy(); + void readUnknownElement(); + void readStudyCurrentVersion(); + void readPack(); + void readC(); + void readR(); + void fixupIncorrectGradeIn1_4(StudyRecord& study); + +private: + static bool equalDouble(double a, double b); + +private: + Dictionary* m_dict; + QString m_studyVersion; + CardPack* m_cardPack; + QString m_cardId; + StudySettings* settings; +}; + +#endif // STUDYFILEREADER_H diff --git a/src/study/StudyFileWriter.cpp b/src/study/StudyFileWriter.cpp new file mode 100644 index 0000000..ab68d6f --- /dev/null +++ b/src/study/StudyFileWriter.cpp @@ -0,0 +1,68 @@ +#include "StudyFileWriter.h" +#include "StudySettings.h" +#include "../dictionary/Dictionary.h" +#include "../version.h" +#include "../dictionary/CardPack.h" + +StudyFileWriter::StudyFileWriter( const Dictionary* aDict ): + m_dict( aDict ) +{ + setAutoFormatting( true ); +} + +bool StudyFileWriter::write( QIODevice* aDevice ) +{ + setDevice( aDevice ); + writeStartDocument(); + writeDTD( "" ); + writeStartElement("study"); + writeAttribute( "version", STUDY_VERSION ); + + foreach( CardPack* pack, m_dict->cardPacks() ) + writePack( pack ); + + writeEndDocument(); + return true; +} + +void StudyFileWriter::writePack( const CardPack* aPack ) + { + QStringList cardIds = aPack->getCardQuestions(); + if( cardIds.isEmpty() ) + return; // Don't write empty pack + writeStartElement( "pack" ); + writeAttribute( "id", aPack->id() ); + if( !aPack->curCardName().isEmpty() ) + { + writeEmptyElement( "cur-card" ); + writeAttribute( "id", aPack->curCardName() ); + } + foreach( QString cardId, cardIds ) + writeCard( cardId, aPack ); + writeEndElement(); + } + +void StudyFileWriter::writeCard( const QString& aCardId, const CardPack* aPack ) + { + QList studyRecords( aPack->getStudyRecords( aCardId ) ); + if( studyRecords.isEmpty() ) // Don't write cards without records + return; + writeStartElement( "c" ); + writeAttribute( "id", aCardId ); + // Take study records from the list in reverse order. The first is the most recent. + QListIterator it( studyRecords ); + it.toBack(); + while( it.hasPrevious() ) + { + StudyRecord record = it.previous(); + writeEmptyElement( "r" ); + writeAttribute( "d", record.date.toString( Qt::ISODate ) ); + writeAttribute( "l", QString::number( record.level ) ); + writeAttribute( "g", QString::number( record.grade ) ); + writeAttribute( "e", QString::number( record.easiness ) ); + writeAttribute( "rt", QString::number( record.recallTime, 'g', 4 ) ); + writeAttribute( "at", QString::number( record.answerTime, 'g', 4 ) ); + writeAttribute( "i", QString::number( record.interval, 'g', 6 ) ); + } + writeEndElement(); // + } diff --git a/src/study/StudyFileWriter.h b/src/study/StudyFileWriter.h new file mode 100644 index 0000000..8800846 --- /dev/null +++ b/src/study/StudyFileWriter.h @@ -0,0 +1,26 @@ +#ifndef STUDYFILEWRITER_H +#define STUDYFILEWRITER_H + +#include +#include +#include + +#include "StudyRecord.h" + +class Dictionary; +class CardPack; + +class StudyFileWriter : public QXmlStreamWriter +{ +public: + StudyFileWriter( const Dictionary* aDict ); + bool write( QIODevice* aDevice ); + +private: + void writePack( const CardPack* aPack ); + void writeCard(const QString& aCardId, const CardPack* aPack ); +private: + const Dictionary* m_dict; +}; + +#endif // STUDYFILEWRITER_H diff --git a/src/study/StudyRecord.cpp b/src/study/StudyRecord.cpp new file mode 100644 index 0000000..330f572 --- /dev/null +++ b/src/study/StudyRecord.cpp @@ -0,0 +1,129 @@ +#include "StudyRecord.h" + +#include "StudySettings.h" +#include "../utils/TimeProvider.h" + +ostream& operator<<(ostream& os, const StudyRecord& study) +{ + if(study.date <= QDateTime::currentDateTime()) + { + const char* dateStr = study.date.toString(Qt::ISODate).toStdString().c_str(); + os << "(" << dateStr << + ", g" << study.grade << ", e" << study.easiness << ", " << + "i" << study.interval << ")"; + } + else + os << "(New card)"; + return os; +} + +// Create "new" study record +StudyRecord::StudyRecord(): + date(QDateTime()), + level(New), + interval(0), + grade(Unknown), + easiness(StudySettings::inst()->initEasiness), + recallTime(0), + answerTime(0) + { + } + +StudyRecord::StudyRecord(int level, int grade, double easiness, double interval): + date(QDateTime()), + recallTime(0), + answerTime(0) +{ + this->level = level; + this->interval = interval; + this->grade = grade; + this->easiness = easiness; +} + +bool StudyRecord::operator==(const StudyRecord& aOther) const + { + return level == aOther.level && + date == aOther.date && + interval == aOther.interval && + grade == aOther.grade && + easiness == aOther.easiness; + } + +bool StudyRecord::timeTriggered() const +{ + if(date.isNull()) + return false; + QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) ); + return nextRepetition <= TimeProvider::get(); +} + +int StudyRecord::getSecsToNextRepetition() const +{ + if(date.isNull()) + return 0; + QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) ); + return TimeProvider::get().secsTo(nextRepetition); +} + +bool StudyRecord::isOneDayOld() const +{ + return date.secsTo(TimeProvider::get()) / (60*60*24.) >= + StudySettings::inst()->nextDayInterval; +} + +bool StudyRecord::isLearning() const +{ + return (level == ShortLearning || level == LongLearning) && + !isOneDayOld(); +} + +int StudyRecord::getScheduledTodayReviews() const +{ + switch(level) + { + case New: + return 3; + case ShortLearning: + return 2; + case LongLearning: + return 1; + default: + return 0; + } +} + +bool StudyRecord::isReviewedToday() const +{ + QDateTime recShiftedDate = shiftedDate(date); + QDateTime curShiftedDate = shiftedDate(QDateTime::currentDateTime()); + return recShiftedDate.date() == curShiftedDate.date(); +} + +bool StudyRecord::isActivatedToday() const +{ + if(date.isNull()) + return false; + QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) ); + QDateTime shiftedNextRep = shiftedDate(nextRepetition); + QDateTime curShiftedDate = shiftedDate(QDateTime::currentDateTime()); + return shiftedNextRep.date() <= curShiftedDate.date(); +} + +QDateTime StudyRecord::shiftedDate(const QDateTime& aDate) +{ + return aDate.addSecs(-60 * 60 * StudySettings::inst()->dayShift); +} + +void StudyRecord::setRecallTime(double time) +{ + recallTime = time; + if(recallTime > MaxAnswerTime) + recallTime = MaxAnswerTime; +} + +void StudyRecord::setAnswerTime(double time) +{ + answerTime = time; + if(answerTime > MaxAnswerTime) + answerTime = MaxAnswerTime; +} diff --git a/src/study/StudyRecord.h b/src/study/StudyRecord.h new file mode 100644 index 0000000..abe7ef9 --- /dev/null +++ b/src/study/StudyRecord.h @@ -0,0 +1,62 @@ +#ifndef STUDYRECORD_H +#define STUDYRECORD_H + +#include +#include + +using std::ostream; + +class ScheduleParams; + +struct StudyRecord +{ +public: + static const int MaxAnswerTime = 180; // sec + enum Level + { + New = 1, + ShortLearning = 2, + LongLearning = 3, + Repeating = 10 + }; + enum Grade + { + Unknown = 1, + Incorrect = 2, + Difficult = 3, + Good = 4, + Easy = 5, + GradesNum = 5 + }; + +public: + StudyRecord(); + StudyRecord(int level, int grade, double easiness, double interval); + + bool timeTriggered() const; + int getSecsToNextRepetition() const; + bool isOneDayOld() const; + bool isLearning() const; + int getScheduledTodayReviews() const; + bool isReviewedToday() const; + bool isActivatedToday() const; + bool operator==( const StudyRecord& aOther ) const; + void setRecallTime(double time); + void setAnswerTime(double time); + +private: + static QDateTime shiftedDate(const QDateTime& aDate); + +public: + QDateTime date; + int level; + double interval; // days + int grade; + double easiness; + double recallTime; // sec + double answerTime; // Full answer time: recall + grading +}; + +ostream& operator<<(ostream& os, const StudyRecord& study); + +#endif diff --git a/src/study/StudySettings.cpp b/src/study/StudySettings.cpp new file mode 100644 index 0000000..c680f8e --- /dev/null +++ b/src/study/StudySettings.cpp @@ -0,0 +1,93 @@ +#include "StudySettings.h" + +#include +#include +#include + +StudySettings::StudySettings() +{ + initDefaultStudy(); +} + +void StudySettings::initDefaultStudy() +{ + showRandomly = true; + newCardsShare = 0.2; + schedRandomness = 0.1; + cardsDayLimit = 80; + newCardsDayLimit = 10; + limitForAddingNewCards = 70; + dayShift = 3; + + initEasiness = 2.5; + minEasiness = 1.3; + maxEasiness = 3.2; + difficultDelta = -0.14; + easyDelta = 0.1; + + unknownInterval = 20./(24*60*60); + incorrectInterval = 1./(24*60); + learningInterval = 10./(24*60); + nextDayInterval = 0.9; + twoDaysInterval = 1.9; +} + +StudySettings* StudySettings::inst() + { + static StudySettings instance; + return &instance; + } + +void StudySettings::load() + { + QSettings settings; + settings.beginGroup("Study"); + loadStudy(settings); + settings.endGroup(); + } + +void StudySettings::loadStudy(const QSettings& settings) +{ + showRandomly = settings.value("random", showRandomly).toBool(); + newCardsShare = settings.value("new-cards-share", newCardsShare).toDouble(); + schedRandomness = + settings.value("scheduling-randomness", schedRandomness).toDouble(); + cardsDayLimit = settings.value("cards-daylimit", cardsDayLimit).toInt(); + newCardsDayLimit = settings.value("new-cards-daylimit", newCardsDayLimit).toInt(); + limitForAddingNewCards = settings.value("limit-for-adding-new-cards", limitForAddingNewCards).toInt(); + dayShift = settings.value("dayshift", dayShift).toInt(); + + initEasiness = settings.value("init-easiness", initEasiness).toDouble(); + minEasiness = settings.value("min-easiness", minEasiness).toDouble(); + maxEasiness = settings.value("max-easiness", maxEasiness).toDouble(); + difficultDelta = settings.value("difficult-delta", difficultDelta).toDouble(); + easyDelta = settings.value("easy-delta", easyDelta).toDouble(); + unknownInterval = settings.value("unknown-interval", unknownInterval).toDouble(); + incorrectInterval = settings.value("incorrect-interval", incorrectInterval).toDouble(); + learningInterval = settings.value("learning-interval", learningInterval).toDouble(); + nextDayInterval = settings.value("next-day-interval", nextDayInterval).toDouble(); +} + +void StudySettings::save() + { + QSettings settings; + settings.beginGroup("Study"); + settings.remove(""); // Remove old user settings + StudySettings defaults; + if(showRandomly != defaults.showRandomly) + settings.setValue( "random", showRandomly ); + if(newCardsShare != defaults.newCardsShare) + settings.setValue( "new-cards-share", newCardsShare ); + if(schedRandomness != defaults.schedRandomness) + settings.setValue( "scheduling-randomness", schedRandomness ); + if(cardsDayLimit != defaults.cardsDayLimit) + settings.setValue( "cards-daylimit",cardsDayLimit ); + if(newCardsDayLimit != defaults.newCardsDayLimit) + settings.setValue( "new-cards-daylimit", newCardsDayLimit ); + if(limitForAddingNewCards != defaults.limitForAddingNewCards) + settings.setValue( "limit-for-adding-new-cards", limitForAddingNewCards ); + if(dayShift != defaults.dayShift) + settings.setValue( "dayshift", dayShift ); + settings.endGroup(); +} + diff --git a/src/study/StudySettings.h b/src/study/StudySettings.h new file mode 100644 index 0000000..d65e8a7 --- /dev/null +++ b/src/study/StudySettings.h @@ -0,0 +1,40 @@ +#ifndef STUDYSETTINGS_H +#define STUDYSETTINGS_H + +#include + +class StudySettings +{ +public: + static StudySettings* inst(); + +public: + StudySettings(); + void save(); + void load(); + +private: + void loadUserSettings(); + void initDefaultStudy(); + void loadStudy(const QSettings& settings); + +public: + bool showRandomly; + double newCardsShare; + double schedRandomness; + int cardsDayLimit; + int newCardsDayLimit; + int limitForAddingNewCards; + int dayShift; // in hours + double initEasiness; + double minEasiness; + double maxEasiness; + double difficultDelta; + double easyDelta; + double unknownInterval; + double incorrectInterval; + double learningInterval; // Long learning level + double nextDayInterval; // first repetition + double twoDaysInterval; // easy first repetition +}; +#endif diff --git a/src/study/WarningPanel.cpp b/src/study/WarningPanel.cpp new file mode 100644 index 0000000..9bf6122 --- /dev/null +++ b/src/study/WarningPanel.cpp @@ -0,0 +1,49 @@ +#include "WarningPanel.h" + +WarningPanel::WarningPanel(QWidget* parent): + QFrame(parent), + warningLabel(new QLabel) +{ + initPanel(); + setLayout(createWarningLayout()); +} + +void WarningPanel::initPanel() +{ + setFrameStyle(QFrame::StyledPanel); + setPalette(QPalette("#ffffcc")); + setAutoFillBackground(true); + hide(); +} + +QHBoxLayout* WarningPanel::createWarningLayout() +{ + QHBoxLayout* warningLt = new QHBoxLayout; + warningLt->addWidget(createWarningIconLabel(), 0, Qt::AlignTop); + warningLt->addWidget(warningLabel, 1); + warningLt->addWidget(createWarningCloseButton(), 0, Qt::AlignTop); + return warningLt; +} + +void WarningPanel::setText(const QString& text) +{ + warningLabel->setText(text); +} + +QLabel* WarningPanel::createWarningIconLabel() const +{ + QLabel* label = new QLabel; + label->setPixmap( QPixmap(":/images/warning.png").scaled( + 24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation ) ); + return label; +} + +QToolButton* WarningPanel::createWarningCloseButton() const +{ + QToolButton* button = new QToolButton; + button->setAutoRaise(true); + button->setIcon(QIcon(":/images/gray-cross.png")); + button->setShortcut(Qt::Key_Escape); + connect(button, SIGNAL(clicked()), this, SLOT(hide())); + return button; +} diff --git a/src/study/WarningPanel.h b/src/study/WarningPanel.h new file mode 100644 index 0000000..f207843 --- /dev/null +++ b/src/study/WarningPanel.h @@ -0,0 +1,25 @@ +#ifndef WARNING_PANEL_H +#define WARNING_PANEL_H + +#include + +class WarningPanel: public QFrame +{ + Q_OBJECT + +public: + WarningPanel(QWidget* parent = 0); + void setText(const QString& text); + +private: + QLabel* createWarningIconLabel() const; + QToolButton* createWarningCloseButton() const; + QHBoxLayout* createWarningLayout(); + void initPanel(); + +private: + QLabel* warningLabel; + +}; + +#endif diff --git a/src/study/WordDrillModel.cpp b/src/study/WordDrillModel.cpp new file mode 100644 index 0000000..403535a --- /dev/null +++ b/src/study/WordDrillModel.cpp @@ -0,0 +1,163 @@ +#include "WordDrillModel.h" +#include "../dictionary/Card.h" +#include "../dictionary/CardPack.h" +#include "StudySettings.h" + +#include +#include + +WordDrillModel::WordDrillModel( CardPack* aCardPack ): + IStudyModel( aCardPack ), m_historyCurPackStart(0) + { + curCardNum = NoCardIndex; + srand( time(NULL) ); + connect( aCardPack, SIGNAL(cardsGenerated()), SLOT(updateStudyState()) ); + + generateFreshPack(); + pickNextCard(); + } + +/** Get current card. + * If the current card doesn't exist any more, find the next valid card. + */ + +Card* WordDrillModel::getCurCard() const + { + if( m_cardHistory.isEmpty() || curCardNum < 0 ) // No cards in the pack + return NULL; + if( curCardNum >= m_cardHistory.size() ) + return NULL; + QString cardName = m_cardHistory.at(curCardNum); + return cardPack->getCard( cardName ); + } + +void WordDrillModel::updateStudyState() + { + generateFreshPack(); + cleanHistoryFromRemovedCards(); + if( getCurCard() ) + emit curCardUpdated(); + else + pickNextCard(); + } + +void WordDrillModel::generateFreshPack() + { + m_freshPack = cardPack->getCardQuestions(); + // Remove already reviewed cards + if( m_historyCurPackStart >= m_cardHistory.size() ) + return; + foreach( QString reviewedCard, m_cardHistory.mid( m_historyCurPackStart ) ) + m_freshPack.removeAll( reviewedCard ); + } + +void WordDrillModel::cleanHistoryFromRemovedCards() + { + if( m_cardHistory.isEmpty() ) + return; + bool cardsRemoved = false; + QMutableStringListIterator it( m_cardHistory ); + int i = 0; + while( it.hasNext() ) + { + QString cardName = it.next(); + if( !cardPack->containsQuestion( cardName ) ) + { + it.remove(); + cardsRemoved = true; + if( i < curCardNum ) + curCardNum--; + } + i++; + } + if( cardsRemoved ) + { + if( curCardNum >= m_cardHistory.size() ) + curCardNum = m_cardHistory.size() - 1; + emit nextCardSelected(); + } + } + + +/** Picks a random card. Removes the selected card from the fresh pack and adds it to the history. + * Thus, the new card is the last entry in the history. + * This function guarantees that the new card's question will be different from the previous one, unless there is no choice. + * + * Updates #m_curCardNum. + */ +void WordDrillModel::pickNextCard() + { + QString selectedCardName; + const Card* selectedCard = NULL; + do + { + if( m_freshPack.isEmpty() ) // No fresh cards + { + m_historyCurPackStart = m_cardHistory.size(); // Refers beyond the history pack + generateFreshPack(); + if( m_freshPack.isEmpty() ) // Still no any cards available - no useful cards in the dictionary or it's empty + { + curCardNum = NoCardIndex; + emit nextCardSelected(); + return; + } + } + if( m_freshPack.size() == 1 ) // Only 1 fresh card, no choice + selectedCardName = m_freshPack.takeFirst(); + else + { + int selectedCardNum; + if( StudySettings::inst()->showRandomly ) + selectedCardNum = rand() % m_freshPack.size(); + else + selectedCardNum = 0; + if( !m_cardHistory.isEmpty() ) + while( m_freshPack[ selectedCardNum ] == m_cardHistory.last() ) // The new question must be different from the current one + { + selectedCardNum++; + selectedCardNum %= m_freshPack.size(); + } + selectedCardName = m_freshPack.takeAt( selectedCardNum ); + } + selectedCard = cardPack->getCard( selectedCardName ); + } + while( !selectedCard ); + m_cardHistory << selectedCardName; + curCardNum = m_cardHistory.size() - 1; + emit nextCardSelected(); + } + +/** Go back along the history line. + * @return true, if the transition was successful + */ +bool WordDrillModel::goBack() + { + if( !canGoBack() ) + return false; + curCardNum--; + emit nextCardSelected(); + return true; + } + +/** Go forward along the history line. + * @return true, if the transition was successful + */ +bool WordDrillModel::goForward() + { + if( !canGoForward() ) + return false; + curCardNum++; + emit nextCardSelected(); + return true; + } + +bool WordDrillModel::canGoBack() + { + return curCardNum > 0; + } + +bool WordDrillModel::canGoForward() + { + return curCardNum < m_cardHistory.size() - 1; + } + diff --git a/src/study/WordDrillModel.h b/src/study/WordDrillModel.h new file mode 100644 index 0000000..7db780a --- /dev/null +++ b/src/study/WordDrillModel.h @@ -0,0 +1,62 @@ +#ifndef WORDDRILLMODEL_H +#define WORDDRILLMODEL_H + +#include "IStudyModel.h" + +#include + +/** Model of Word Drill study tool. + * + * At the beginning of a test, the model generates a fresh pack of valid cards: #m_freshPack. In course of the test, + * cards one-by-one are randomly taken from the fresh pack and moved to the history pack #m_cardHistory. + * The part of history, which belongs to the current pack (recently generated new pack), is separated from older + * cards with property #m_historyCurPackStart. + * The current card can be obtained with #curCard(), it is taken from the history. When the fresh pack is finished + * and the next card required, new fresh pack is generated. + * + * All used cards over all pack iterations are saved to the history. + * The user can travel back and forth along the history with goBack() and goForward(). In this case #m_curCardNum + * increases or decreases. + */ + +class WordDrillModel: public IStudyModel +{ +Q_OBJECT + +public: + WordDrillModel( CardPack* aCardPack ); + + Card* getCurCard() const; + bool canGoBack(); + bool canGoForward(); + + int getCurCardNum() const { return curCardNum; } + int historySize() const {return m_cardHistory.size(); } + +private: + void generateFreshPack(); + void cleanHistoryFromRemovedCards(); + +public slots: + void pickNextCard(); + bool goBack(); + bool goForward(); + +private slots: + void updateStudyState(); + +private: + static const int NoCardIndex = -1; + +private: + /** Fresh cards. The cards that were not shown yet (in the current iteration). Not own.*/ + QStringList m_freshPack; + + /// List of all shown cards. Not own. + QStringList m_cardHistory; + + /// The start index of the current pack in the history + int m_historyCurPackStart; +}; + +#endif diff --git a/src/study/WordDrillWindow.cpp b/src/study/WordDrillWindow.cpp new file mode 100644 index 0000000..dd13bd1 --- /dev/null +++ b/src/study/WordDrillWindow.cpp @@ -0,0 +1,180 @@ +#include "WordDrillWindow.h" +#include "WordDrillModel.h" +#include "../dictionary/Card.h" +#include "../dictionary/CardPack.h" +#include "CardSideView.h" + +WordDrillWindow::WordDrillWindow( WordDrillModel* aModel, QWidget* aParent ): + IStudyWindow(aModel, tr("Word drill"), aParent) +{ + studyType = AppModel::WordDrill; + createUI(); // Can't call from parent constructor + setWindowIcon( QIcon(":/images/word-drill.png") ); + showNextCard(); +} + +WordDrillWindow::~WordDrillWindow() +{ + WriteSettings(); +} + +QVBoxLayout* WordDrillWindow::createLowerPanel() +{ + QHBoxLayout* cardNumLayout = new QHBoxLayout; + iCardNumLabel = new QLabel; + iCardNumLabel->setAlignment( Qt::AlignVCenter ); + iCardNumLabel->setToolTip(tr("Current card / All cards")); + iProgressBar = new QProgressBar; + iProgressBar->setTextVisible( false ); + iProgressBar->setMaximumHeight( iProgressBar->sizeHint().height()/2 ); + iProgressBar->setToolTip(tr("Progress of reviewing cards")); + iShowAnswersCB = new QCheckBox(tr("Show answers")); + iShowAnswersCB->setShortcut( tr("S", "Shortcut for 'Show answers' checkbox") ); + iShowAnswersCB->setChecked( true ); + connect( iShowAnswersCB, SIGNAL(stateChanged(int)), this, SLOT(ToggleAnswer()) ); + + cardNumLayout->addWidget( iCardNumLabel ); + cardNumLayout->addWidget( iProgressBar ); + cardNumLayout->addWidget( iShowAnswersCB ); + + iBackBtn = new QPushButton( QIcon(":/images/back.png"), tr("Back"), this); + iBackBtn->setShortcut( Qt::Key_Left ); + iBackBtn->setToolTip( tr("Go back in history") + ShortcutToStr( iBackBtn ) ); + connect( iBackBtn, SIGNAL(clicked()), qobject_cast(m_model), SLOT(goBack()) ); + + iForwardBtn = new QPushButton( QIcon(":/images/forward.png"), tr("Forward"), this); + iForwardBtn->setShortcut( Qt::Key_Right ); + iForwardBtn->setToolTip( tr("Go forward in history") + ShortcutToStr( iForwardBtn ) ); + connect( iForwardBtn, SIGNAL(clicked()), qobject_cast(m_model), SLOT(goForward()) ); + + iNextBtn = new QPushButton( QIcon(":/images/next.png"), tr("Next"), this); + iNextBtn->setShortcut( Qt::Key_Return ); + iNextBtn->setToolTip( tr("Show next card (Enter)") ); + connect( iNextBtn, SIGNAL(clicked()), qobject_cast(m_model), SLOT(pickNextCard()) ); + + QHBoxLayout* controlLt = new QHBoxLayout; + controlLt->addWidget( iBackBtn ); + controlLt->addWidget( iForwardBtn ); + controlLt->addWidget( iNextBtn ); + + QVBoxLayout* lowerPanelLt = new QVBoxLayout; + lowerPanelLt->addLayout( cardNumLayout ); + if( bigScreen() ) + lowerPanelLt->addLayout( controlLt ); + + return lowerPanelLt; +} + +void WordDrillWindow::setStateForNextCard() +{ + if( m_model->getCurCard() ) + state = iShowAnswersCB->isChecked()? StateAnswerVisible: StateAnswerHidden; + else + state = StateNoCards; + } + +void WordDrillWindow::processState() +{ +if( state == StateAnswerHidden || state == StateAnswerVisible ) + { + centralStackedLt->setCurrentIndex( CardPage ); + DisplayCardNum(); + UpdateButtons(); + } +switch( state ) + { + case StateAnswerHidden: + questionLabel->setQuestion( m_model->getCurCard()->getQuestion() ); + answerStackedLt->setCurrentIndex( AnsButtonPage ); + answerBtn->setFocus(); + break; + case StateAnswerVisible: + { + Card* card = m_model->getCurCard(); + if( iShowAnswersCB->isChecked() ) // StateAnswerHidden was skipped + questionLabel->setQuestion( card->getQuestion() ); + answerStackedLt->setCurrentIndex( AnsLabelPage ); + answerLabel->setQstAnsr( card->getQuestion(), card->getAnswers() ); + iNextBtn->setFocus(); + break; + } + case StateNoCards: + centralStackedLt->setCurrentIndex(MessagePage); + messageLabel->setText( + QString("
") + + tr("No cards available") + "
"); + break; + } +} + +void WordDrillWindow::DisplayCardNum() +{ +int curCardIndex = qobject_cast(m_model)->getCurCardNum(); +int curCardNum = curCardIndex + 1; +int historySize = qobject_cast(m_model)->historySize(); +int packSize = m_model->getCardPack()->cardsNum(); + +int curCardNumInPack = curCardIndex % packSize + 1; +int passNum; +if( packSize > 0 ) + passNum = curCardIndex / packSize + 1; +else + passNum = 1; +QString cardNumStr = QString("%1/%2").arg( curCardNumInPack ).arg( packSize ); +QString passIconStr = ""; +if( passNum > 1 ) + cardNumStr += " " + passIconStr + QString::number( passNum ); +if( curCardNum < historySize && packSize > 0 ) + { + int lastCardIndex = historySize - 1; + int lastCardNumInPack = lastCardIndex % packSize + 1; + cardNumStr += " (" + QString::number( lastCardNumInPack ); + int sessionPassNum = lastCardIndex / packSize + 1; + if( sessionPassNum > 1 ) + cardNumStr += " " + passIconStr + QString::number( sessionPassNum ); + cardNumStr += ")"; + } + +iCardNumLabel->setText( cardNumStr ); +iProgressBar->setMaximum( packSize ); +iProgressBar->setValue( curCardNumInPack ); +} + +void WordDrillWindow::UpdateButtons() +{ +iBackBtn->setEnabled( qobject_cast(m_model)->canGoBack() ); +iForwardBtn->setEnabled( qobject_cast(m_model)->canGoForward() ); +} + +void WordDrillWindow::ToggleAnswer() +{ +if( !m_model->getCurCard() ) + return; +answerStackedLt->setCurrentIndex( iShowAnswersCB->isChecked()? AnsLabelPage: AnsButtonPage ); +switch( state ) + { + case StateAnswerHidden: + state = StateAnswerVisible; + break; + case StateAnswerVisible: + state = iShowAnswersCB->isChecked()? StateAnswerVisible: StateAnswerHidden; + break; + default: + return; + } +processState(); +} + +void WordDrillWindow::ReadSettings() +{ +QSettings settings; +move( settings.value("worddrill-pos", QPoint(PosX, PosY)).toPoint() ); +resize( settings.value("worddrill-size", QSize(Width, Height)).toSize() ); +} + +void WordDrillWindow::WriteSettings() +{ +QSettings settings; +settings.setValue("worddrill-pos", pos()); +settings.setValue("worddrill-size", size()); +} diff --git a/src/study/WordDrillWindow.h b/src/study/WordDrillWindow.h new file mode 100644 index 0000000..ed421e9 --- /dev/null +++ b/src/study/WordDrillWindow.h @@ -0,0 +1,46 @@ +#ifndef WORDDRILLWINDOW_H +#define WORDDRILLWINDOW_H + +#include +#include "IStudyWindow.h" + +class WordDrillModel; + +class WordDrillWindow: public IStudyWindow +{ + Q_OBJECT + +public: + WordDrillWindow( WordDrillModel* aModel, QWidget* aParent ); + ~WordDrillWindow(); + +protected: + QVBoxLayout* createLowerPanel(); + void setStateForNextCard(); + void processState(); + void ReadSettings(); + void WriteSettings(); + +private: + void DisplayCardNum(); + void UpdateButtons(); + +private slots: + void ToggleAnswer(); + +private: + static const int PosX = 200; + static const int PosY = 200; + static const int Width = 600; + static const int Height = 350; + +private: + QLabel* iCardNumLabel; + QProgressBar* iProgressBar; + QCheckBox* iShowAnswersCB; + QPushButton* iBackBtn; + QPushButton* iForwardBtn; + QPushButton* iNextBtn; +}; + +#endif -- cgit