summaryrefslogtreecommitdiff
path: root/src/study
diff options
context:
space:
mode:
authorJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
committerJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
commitd24f813f3f2a05c112e803e4256b53535895fc98 (patch)
tree601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9 /src/study
Initial mirror commitHEADmaster
Diffstat (limited to 'src/study')
-rw-r--r--src/study/CardEditDialog.cpp119
-rw-r--r--src/study/CardEditDialog.h42
-rw-r--r--src/study/CardSideView.cpp205
-rw-r--r--src/study/CardSideView.h52
-rw-r--r--src/study/CardsStatusBar.cpp82
-rw-r--r--src/study/CardsStatusBar.h37
-rw-r--r--src/study/IStudyModel.cpp9
-rw-r--r--src/study/IStudyModel.h35
-rw-r--r--src/study/IStudyWindow.cpp293
-rw-r--r--src/study/IStudyWindow.h107
-rw-r--r--src/study/NumberFrame.cpp40
-rw-r--r--src/study/NumberFrame.h29
-rw-r--r--src/study/SpacedRepetitionModel.cpp390
-rw-r--r--src/study/SpacedRepetitionModel.h79
-rw-r--r--src/study/SpacedRepetitionWindow.cpp380
-rw-r--r--src/study/SpacedRepetitionWindow.h89
-rw-r--r--src/study/StudyFileReader.cpp238
-rw-r--r--src/study/StudyFileReader.h44
-rw-r--r--src/study/StudyFileWriter.cpp68
-rw-r--r--src/study/StudyFileWriter.h26
-rw-r--r--src/study/StudyRecord.cpp129
-rw-r--r--src/study/StudyRecord.h62
-rw-r--r--src/study/StudySettings.cpp93
-rw-r--r--src/study/StudySettings.h40
-rw-r--r--src/study/WarningPanel.cpp49
-rw-r--r--src/study/WarningPanel.h25
-rw-r--r--src/study/WordDrillModel.cpp163
-rw-r--r--src/study/WordDrillModel.h62
-rw-r--r--src/study/WordDrillWindow.cpp180
-rw-r--r--src/study/WordDrillWindow.h46
30 files changed, 3213 insertions, 0 deletions
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 <QSettings>
+#include <QCloseEvent>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QVariant>
+
+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<const CardPack*>(aCurCard->getCardPack());
+ Q_ASSERT(cardPack);
+ m_dictionary = static_cast<const Dictionary*>(cardPack->dictionary());
+ Q_ASSERT( m_dictionary );
+ foreach( const DicRecord* record, aCurCard->getSourceRecords() )
+ {
+ int row = m_dictionary->indexOfRecord( const_cast<DicRecord*>( 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 <QDialog>
+#include <QEvent>
+
+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 <QtCore>
+
+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 += "<br/>";
+ if(!text.isEmpty())
+ formattedAnswers << text;
+ i++;
+ }
+ return formattedAnswers.join("<br/>");
+}
+
+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<IDictionary*>(cardPack->dictionary())->
+ extendImagePaths( aField );
+
+ FieldStyle fieldStyle = FieldStyleFactory::inst()->getStyle( aStyle );
+ QString beginning("<span style=\"");
+ beginning += QString("font-family:'%1'").arg( fieldStyle.font.family() );
+ beginning += QString("; font-size:%1pt").arg( fieldStyle.font.pointSize() );
+ beginning += getHighlighting(fieldStyle);
+ beginning += "\">" + fieldStyle.prefix;
+ QString ending( fieldStyle.suffix + "</span>");
+
+ // 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("<span style=\"");
+ spanBegin += getHighlighting(fieldStyle.getKeywordStyle()) + "\">";
+ resText.replace('[', spanBegin);
+ resText.replace( ']', "</span>" );
+ 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 <QLabel>
+
+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 <QtWidgets>
+
+class CardsStatusBar : public QWidget
+{
+ Q_OBJECT
+public:
+ static const QStringList Colors;
+
+public:
+ CardsStatusBar(QWidget* aParent = 0);
+ ~CardsStatusBar();
+ void setValues(const QList<int>& 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<int> 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 <QObject>
+
+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 <QCloseEvent>
+#include <QMessageBox>
+
+#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<MainWindow*>( 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<Dictionary*>(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<Dictionary*>(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<Dictionary*>(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<Dictionary*>(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 <QtCore>
+#include <QtWidgets>
+
+#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 <QtWidgets>
+
+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 <time.h>
+#include <QtDebug>
+#include <QtCore>
+
+#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<int> 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 <QMultiMap>
+#include <QTime>
+
+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<int> 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 += "&nbsp;";
+ QString colorBoxPattern = "<p><span style=\"background-color: %1;\">" +
+ boxSpace + "</span> &nbsp;";
+ QString pEnd = "</p>";
+
+ 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<SpacedRepetitionModel*>(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("<p>" + 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<int> visibleGrades = static_cast<SpacedRepetitionModel*>(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<int> visibleGrades = static_cast<SpacedRepetitionModel*>(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<SpacedRepetitionModel*>(m_model)->isNew());
+}
+
+void SpacedRepetitionWindow::displayAnswer()
+{
+ static_cast<SpacedRepetitionModel*>(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<SpacedRepetitionModel*>(m_model)->isNew())
+ {
+ exactAnswerLabel->hide();
+ return;
+ }
+ else
+ exactAnswerLabel->show();
+ bool isCorrect = correctAnswers.first() == exactAnswerEdit->text().trimmed();
+ QString beginning("<span style=\"");
+ beginning += QString("color:%1").arg(isCorrect ? "green" : "red");
+ beginning += "\">";
+ QString answer = beginning + exactAnswerEdit->text() + "</span>";
+ 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<SpacedRepetitionModel*>(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("<div align=\"center\" style=\"font-size: 22pt; font-weight: bold\">") +
+ tr("All cards are reviewed") +
+ "</div>" + "<p align=\"center\" style=\"font-size: 11pt\">" +
+ tr("You can go to the next pack or dictionary, or open the Word drill.") +
+ "</p>");
+}
+
+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 <QtCore>
+#include <QtWidgets>
+
+#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 <QMessageBox>
+
+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 <QXmlStreamReader>
+#include <QList>
+#include <QString>
+
+#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( "<!DOCTYPE freshmemory-study>" );
+ 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<StudyRecord> 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<StudyRecord> 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(); // <c />
+ }
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 <QXmlStreamWriter>
+#include <QString>
+#include <QList>
+
+#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 <iostream>
+#include <QDateTime>
+
+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 <QCoreApplication>
+#include <QStringList>
+#include <QtDebug>
+
+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 <QSettings>
+
+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 <QtWidgets>
+
+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 <stdlib.h>
+#include <time.h>
+
+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 <QStringList>
+
+/** 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<WordDrillModel*>(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<WordDrillModel*>(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<WordDrillModel*>(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("<div align=\"center\" style=\"font-size: 22pt; font-weight: bold\">") +
+ tr("No cards available") + "</div>");
+ break;
+ }
+}
+
+void WordDrillWindow::DisplayCardNum()
+{
+int curCardIndex = qobject_cast<WordDrillModel*>(m_model)->getCurCardNum();
+int curCardNum = curCardIndex + 1;
+int historySize = qobject_cast<WordDrillModel*>(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 = "<img src=\":/images/passes.png\"/>";
+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<WordDrillModel*>(m_model)->canGoBack() );
+iForwardBtn->setEnabled( qobject_cast<WordDrillModel*>(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 <QtWidgets>
+#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