summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/charts/Chart.cpp23
-rw-r--r--src/charts/Chart.h32
-rw-r--r--src/charts/ChartAxes.cpp138
-rw-r--r--src/charts/ChartAxes.h51
-rw-r--r--src/charts/ChartDataLine.cpp80
-rw-r--r--src/charts/ChartDataLine.h37
-rw-r--r--src/charts/ChartMarker.cpp50
-rw-r--r--src/charts/ChartMarker.h36
-rw-r--r--src/charts/ChartScene.cpp111
-rw-r--r--src/charts/ChartScene.h55
-rw-r--r--src/charts/ChartToolTip.cpp43
-rw-r--r--src/charts/ChartToolTip.h26
-rw-r--r--src/charts/ChartView.cpp12
-rw-r--r--src/charts/ChartView.h16
-rw-r--r--src/charts/DataPoint.h15
-rw-r--r--src/charts/PieChart.cpp28
-rw-r--r--src/charts/PieChart.h27
-rw-r--r--src/charts/PieChartScene.cpp30
-rw-r--r--src/charts/PieChartScene.h30
-rw-r--r--src/charts/PieLegend.cpp48
-rw-r--r--src/charts/PieLegend.h34
-rw-r--r--src/charts/PieRound.cpp77
-rw-r--r--src/charts/PieRound.h37
-rw-r--r--src/charts/TimeChart.cpp155
-rw-r--r--src/charts/TimeChart.h54
-rw-r--r--src/dic-options/DictionaryOptionsDialog.cpp62
-rw-r--r--src/dic-options/DictionaryOptionsDialog.h41
-rw-r--r--src/dic-options/DraggableListModel.cpp83
-rw-r--r--src/dic-options/DraggableListModel.h33
-rw-r--r--src/dic-options/FieldStyleDelegate.cpp35
-rw-r--r--src/dic-options/FieldStyleDelegate.h19
-rw-r--r--src/dic-options/FieldsListModel.cpp222
-rw-r--r--src/dic-options/FieldsListModel.h49
-rw-r--r--src/dic-options/FieldsPage.cpp139
-rw-r--r--src/dic-options/FieldsPage.h37
-rw-r--r--src/dic-options/FieldsPreviewModel.cpp34
-rw-r--r--src/dic-options/FieldsPreviewModel.h27
-rw-r--r--src/dic-options/FieldsView.cpp41
-rw-r--r--src/dic-options/FieldsView.h24
-rw-r--r--src/dic-options/PackFieldsListModel.cpp101
-rw-r--r--src/dic-options/PackFieldsListModel.h34
-rw-r--r--src/dic-options/PackFieldsView.cpp36
-rw-r--r--src/dic-options/PackFieldsView.h24
-rw-r--r--src/dic-options/PacksListModel.cpp81
-rw-r--r--src/dic-options/PacksListModel.h32
-rw-r--r--src/dic-options/PacksPage.cpp345
-rw-r--r--src/dic-options/PacksPage.h70
-rw-r--r--src/dic-options/UnusedFieldsListModel.cpp111
-rw-r--r--src/dic-options/UnusedFieldsListModel.h35
-rw-r--r--src/dictionary/Card.cpp125
-rw-r--r--src/dictionary/Card.h50
-rw-r--r--src/dictionary/CardPack.cpp432
-rw-r--r--src/dictionary/CardPack.h144
-rw-r--r--src/dictionary/DicCsvReader.cpp205
-rw-r--r--src/dictionary/DicCsvReader.h41
-rw-r--r--src/dictionary/DicCsvWriter.cpp110
-rw-r--r--src/dictionary/DicCsvWriter.h31
-rw-r--r--src/dictionary/DicRecord.cpp62
-rw-r--r--src/dictionary/DicRecord.h40
-rw-r--r--src/dictionary/Dictionary.cpp601
-rw-r--r--src/dictionary/Dictionary.h187
-rw-r--r--src/dictionary/DictionaryReader.cpp384
-rw-r--r--src/dictionary/DictionaryReader.h44
-rw-r--r--src/dictionary/DictionaryWriter.cpp79
-rw-r--r--src/dictionary/DictionaryWriter.h26
-rw-r--r--src/dictionary/Field.cpp18
-rw-r--r--src/dictionary/Field.h46
-rw-r--r--src/dictionary/ICardPack.cpp23
-rw-r--r--src/dictionary/ICardPack.h33
-rw-r--r--src/dictionary/IDictionary.cpp59
-rw-r--r--src/dictionary/IDictionary.h51
-rw-r--r--src/dictionary/TreeItem.h25
-rw-r--r--src/export-import/CsvData.h36
-rw-r--r--src/export-import/CsvDialog.cpp176
-rw-r--r--src/export-import/CsvDialog.h62
-rw-r--r--src/export-import/CsvExportDialog.cpp137
-rw-r--r--src/export-import/CsvExportDialog.h52
-rw-r--r--src/export-import/CsvImportDialog.cpp155
-rw-r--r--src/export-import/CsvImportDialog.h64
-rw-r--r--src/field-styles/FieldStyle.cpp38
-rw-r--r--src/field-styles/FieldStyle.h28
-rw-r--r--src/field-styles/FieldStyleFactory.cpp145
-rw-r--r--src/field-styles/FieldStyleFactory.h49
-rw-r--r--src/main-view/AboutDialog.cpp27
-rw-r--r--src/main-view/AboutDialog.h17
-rw-r--r--src/main-view/AppModel.cpp196
-rw-r--r--src/main-view/AppModel.h64
-rw-r--r--src/main-view/CardFilterModel.cpp24
-rw-r--r--src/main-view/CardFilterModel.h21
-rw-r--r--src/main-view/CardPreview.cpp55
-rw-r--r--src/main-view/CardPreview.h30
-rw-r--r--src/main-view/DictTableDelegate.cpp128
-rw-r--r--src/main-view/DictTableDelegate.h44
-rw-r--r--src/main-view/DictTableDelegatePainter.cpp61
-rw-r--r--src/main-view/DictTableDelegatePainter.h36
-rw-r--r--src/main-view/DictTableModel.cpp142
-rw-r--r--src/main-view/DictTableModel.h52
-rw-r--r--src/main-view/DictTableView.cpp84
-rw-r--r--src/main-view/DictTableView.h36
-rw-r--r--src/main-view/DictionaryTabWidget.cpp108
-rw-r--r--src/main-view/DictionaryTabWidget.h46
-rw-r--r--src/main-view/FieldContentCodec.cpp52
-rw-r--r--src/main-view/FieldContentCodec.h28
-rw-r--r--src/main-view/FieldContentPainter.h16
-rw-r--r--src/main-view/FindPanel.cpp258
-rw-r--r--src/main-view/FindPanel.h66
-rw-r--r--src/main-view/LanguageMenu.cpp64
-rw-r--r--src/main-view/LanguageMenu.h28
-rw-r--r--src/main-view/MainWindow.cpp1313
-rw-r--r--src/main-view/MainWindow.h218
-rw-r--r--src/main-view/PacksTreeModel.cpp93
-rw-r--r--src/main-view/PacksTreeModel.h32
-rw-r--r--src/main-view/RecentFilesManager.cpp99
-rw-r--r--src/main-view/RecentFilesManager.h43
-rw-r--r--src/main-view/RecordEditor.cpp122
-rw-r--r--src/main-view/RecordEditor.h46
-rw-r--r--src/main-view/UndoCommands.cpp322
-rw-r--r--src/main-view/UndoCommands.h109
-rw-r--r--src/main-view/WelcomeScreen.cpp51
-rw-r--r--src/main-view/WelcomeScreen.h25
-rw-r--r--src/main.cpp102
-rw-r--r--src/main.h8
-rw-r--r--src/settings/ColorBox.cpp36
-rw-r--r--src/settings/ColorBox.h28
-rw-r--r--src/settings/FontColorSettingsDialog.cpp300
-rw-r--r--src/settings/FontColorSettingsDialog.h71
-rw-r--r--src/settings/StudySettingsDialog.cpp141
-rw-r--r--src/settings/StudySettingsDialog.h45
-rw-r--r--src/settings/StylePreviewModel.cpp63
-rw-r--r--src/settings/StylePreviewModel.h32
-rw-r--r--src/settings/StylesListModel.h16
-rw-r--r--src/statistics/BaseStatPage.cpp38
-rw-r--r--src/statistics/BaseStatPage.h31
-rw-r--r--src/statistics/ProgressPage.cpp41
-rw-r--r--src/statistics/ProgressPage.h33
-rw-r--r--src/statistics/ScheduledPage.cpp35
-rw-r--r--src/statistics/ScheduledPage.h22
-rw-r--r--src/statistics/StatisticsParams.cpp2
-rw-r--r--src/statistics/StatisticsParams.h20
-rw-r--r--src/statistics/StatisticsView.cpp182
-rw-r--r--src/statistics/StatisticsView.h53
-rw-r--r--src/statistics/StudiedPage.cpp17
-rw-r--r--src/statistics/StudiedPage.h18
-rw-r--r--src/statistics/TimeChartPage.cpp54
-rw-r--r--src/statistics/TimeChartPage.h33
-rw-r--r--src/strings.cpp6
-rw-r--r--src/strings.h22
-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
-rw-r--r--src/utils/IRandomGenerator.h17
-rw-r--r--src/utils/RandomGenerator.cpp38
-rw-r--r--src/utils/RandomGenerator.h16
-rw-r--r--src/utils/TimeProvider.cpp6
-rw-r--r--src/utils/TimeProvider.h12
-rw-r--r--src/version.cpp3
-rw-r--r--src/version.h19
184 files changed, 15647 insertions, 0 deletions
diff --git a/src/charts/Chart.cpp b/src/charts/Chart.cpp
new file mode 100644
index 0000000..f50911a
--- /dev/null
+++ b/src/charts/Chart.cpp
@@ -0,0 +1,23 @@
+#include "Chart.h"
+#include "ChartScene.h"
+#include "ChartView.h"
+
+Chart::Chart():
+ scene(new ChartScene(this))
+{
+ createChartView();
+}
+
+void Chart::createChartView()
+{
+ view = new ChartView(scene);
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(view);
+ mainLt->setContentsMargins(QMargins());
+ setLayout(mainLt);
+}
+
+void Chart::setDataSet(const QList<DataPoint>& dataSet)
+{
+ scene->setDataSet(dataSet, 1);
+}
diff --git a/src/charts/Chart.h b/src/charts/Chart.h
new file mode 100644
index 0000000..a613b06
--- /dev/null
+++ b/src/charts/Chart.h
@@ -0,0 +1,32 @@
+#ifndef CHART_H
+#define CHART_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartView;
+class ChartScene;
+
+class Chart: public QWidget
+{
+public:
+ Chart();
+ void setLabels(const QString& xLabel, const QString& yLabel)
+ { this->xLabel = xLabel; this->yLabel = yLabel; }
+ QString getXLabel() const { return xLabel; }
+ QString getYLabel() const { return yLabel; }
+ void setDataSet(const QList<DataPoint>& dataSet);
+
+private:
+ void createChartView();
+
+protected:
+ ChartScene* scene;
+ ChartView* view;
+ QString xLabel;
+ QString yLabel;
+};
+
+#endif
diff --git a/src/charts/ChartAxes.cpp b/src/charts/ChartAxes.cpp
new file mode 100644
index 0000000..caf8828
--- /dev/null
+++ b/src/charts/ChartAxes.cpp
@@ -0,0 +1,138 @@
+#include "ChartAxes.h"
+#include "ChartScene.h"
+
+#include <cmath>
+
+const QPointF ChartAxes::XLabelOffset(0, 5);
+const QPointF ChartAxes::YLabelOffset(-10, -7);
+const QFont ChartAxes::TickLabelFont("Sans Serif", 11);
+
+ChartAxes::ChartAxes(ChartScene* scene, int xTickInterval):
+ scene(scene), xTickInterval(xTickInterval)
+{
+}
+
+void ChartAxes::paint()
+{
+ addAxisLines();
+ addTicks();
+}
+
+void ChartAxes::addAxisLines()
+{
+ QPointF chartOrigin = scene->getChartOrigin();
+ QRectF chartRect = scene->getChartRect();
+ scene->addLine(QLineF(chartOrigin, chartRect.topLeft()));
+ scene->addLine(QLineF(chartOrigin, chartRect.bottomRight()));
+}
+
+void ChartAxes::addTicks()
+{
+ addXTicks();
+ addYTicks();
+}
+
+void ChartAxes::addXTicks()
+{
+ qreal tickSpacing = scene->getDataDirection() * scene->getXTickSpacing();
+ QPointF pos = getTickOrigin(XTick);
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ {
+ addLabelledXTick(i, pos);
+ pos.rx() += tickSpacing;
+ }
+}
+
+void ChartAxes::addLabelledXTick(int index, QPointF& pos)
+{
+ if(index % xTickInterval == 0)
+ {
+ addTick(pos, XTick);
+ DataPoint point = scene->getDataSet().at(index);
+ addLabel(point.label, pos, XTick);
+ }
+}
+
+QPointF ChartAxes::getTickOrigin(TickOrientation orientation) const
+{
+ QPointF res = scene->getChartOrigin();
+ if(orientation == XTick)
+ {
+ if(scene->getDataDirection() == 1)
+ res.rx() += ChartScene::DataSetPadding.width();
+ else
+ res.rx() += scene->getChartRect().width();
+ }
+ else
+ res.ry() -= ChartScene::DataSetPadding.height();
+ return res;
+}
+
+void ChartAxes::addTick(const QPointF& pos, TickOrientation orientation)
+{
+ QPointF end = pos + getTickVector(orientation);
+ scene->addLine(QLineF(pos, end));
+}
+
+QPointF ChartAxes::getTickVector(TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return QPointF(0, TickLength);
+ else
+ return QPointF(-TickLength, 0);
+}
+
+void ChartAxes::addLabel(const QString& labelText, const QPointF& pos,
+ TickOrientation orientation)
+{
+ QPointF labelOffset = getLabelOffset(orientation);
+ QGraphicsItem* textItem = scene->addSimpleText(labelText, TickLabelFont);
+ qreal xAlignOffset = getLabelXAlignOffset(
+ textItem->boundingRect().width(), orientation);
+ textItem->setPos(pos + labelOffset + QPointF(xAlignOffset, 0));
+}
+
+QPointF ChartAxes::getLabelOffset(TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return XLabelOffset;
+ else
+ return YLabelOffset;
+}
+
+qreal ChartAxes::getLabelXAlignOffset(qreal labelWidth, TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return -labelWidth / 2;
+ else
+ return -labelWidth;
+}
+
+void ChartAxes::addYTicks()
+{
+ int valueSpacing = scene->getYTickSpacing();
+ qreal posSpacing = scene->getYPosFromValue(valueSpacing);
+ QPointF pos = getTickOrigin(YTick);
+ for(int value = scene->getMinY(); value <= scene->getMaxY();
+ value += valueSpacing)
+ {
+ addLabelledYTick(value, pos, posSpacing);
+ if(valueSpacing == 0)
+ break;
+ }
+}
+
+void ChartAxes::addLabelledYTick(int value, QPointF& pos, qreal posSpacing)
+{
+ addTick(pos, YTick);
+ addLabel(QString::number(value), pos, YTick);
+ addHRuler(pos);
+ pos.ry() -= posSpacing;
+}
+
+void ChartAxes::addHRuler(const QPointF& pos)
+ {
+ QPointF end = pos;
+ end.rx() += scene->getChartRect().width();
+ scene->addLine(QLineF(pos, end), QPen(Qt::lightGray));
+ }
diff --git a/src/charts/ChartAxes.h b/src/charts/ChartAxes.h
new file mode 100644
index 0000000..ebfe4ac
--- /dev/null
+++ b/src/charts/ChartAxes.h
@@ -0,0 +1,51 @@
+#ifndef CHART_AXES_H
+#define CHART_AXES_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class ChartScene;
+
+class ChartAxes
+{
+public:
+ ChartAxes(ChartScene* scene, int xTickInterval);
+ void paint();
+
+private:
+ enum TickOrientation
+ {
+ XTick,
+ YTick
+ };
+
+private:
+ static QPointF getTickVector(TickOrientation orientation);
+ static QPointF getLabelOffset(TickOrientation orientation);
+ static qreal getLabelXAlignOffset(qreal labelWidth, TickOrientation orientation);
+
+private:
+ void addAxisLines();
+ void addTicks();
+ void addXTicks();
+ void addYTicks();
+ void addLabel(const QString& labelText, const QPointF& pos,
+ TickOrientation orientation);
+ QPointF getTickOrigin(TickOrientation orientation) const;
+ void addHRuler(const QPointF& pos);
+ void addTick(const QPointF& pos, TickOrientation orientation);
+ void addLabelledXTick(int index, QPointF& pos);
+ void addLabelledYTick(int value, QPointF& pos, qreal posSpacing);
+
+private:
+ static const int TickLength = 5;
+ static const QPointF XLabelOffset;
+ static const QPointF YLabelOffset;
+ static const QFont TickLabelFont;
+
+private:
+ ChartScene* scene;
+ int xTickInterval;
+};
+
+#endif
diff --git a/src/charts/ChartDataLine.cpp b/src/charts/ChartDataLine.cpp
new file mode 100644
index 0000000..17919cd
--- /dev/null
+++ b/src/charts/ChartDataLine.cpp
@@ -0,0 +1,80 @@
+#include "ChartDataLine.h"
+#include "ChartScene.h"
+#include "ChartMarker.h"
+
+const QColor ChartDataLine::MarkerColor(Qt::blue);
+const QColor ChartDataLine::FillColor(0, 0, 255, 128);
+
+ChartDataLine::ChartDataLine(ChartScene* scene):
+ scene(scene),
+ origin(getDataSetOrigin()),
+ xSpacing(scene->getXTickSpacing())
+{
+}
+
+QPointF ChartDataLine::getDataSetOrigin() const
+{
+ QSizeF padding = scene->DataSetPadding;
+ QPointF res = scene->getChartOrigin();
+ res.ry() -= padding.height();
+ if(scene->getDataDirection() == 1)
+ res.rx() += padding.width();
+ else
+ res.rx() += scene->getChartRect().width();
+ return res;
+}
+
+void ChartDataLine::paint()
+{
+ QPainterPath linePath = createLinePath();
+ addLinePath(linePath);
+ addAreaPath(linePath);
+}
+
+QPainterPath ChartDataLine::createLinePath()
+{
+ QList<DataPoint> dataSet = scene->getDataSet();
+ QPointF firstPoint = getDataPos(0, dataSet.first().value);
+ QPainterPath linePath(firstPoint);
+ for (int i = 0; i < dataSet.size(); i++)
+ addLineSegment(linePath, i);
+ return linePath;
+}
+
+void ChartDataLine::addLineSegment(QPainterPath& path, int i)
+{
+ DataPoint dataPoint = scene->getDataSet().at(i);
+ QPointF pos = getDataPos(i, dataPoint.value);
+ QString toolTipText = createToolTipText(dataPoint);
+ scene->addItem(new ChartMarker(pos, toolTipText));
+ path.lineTo(pos);
+}
+
+QPointF ChartDataLine::getDataPos(int index, int yValue)
+{
+ qreal xPos = scene->getDataDirection() * index * xSpacing;
+ qreal yPos = scene->getYPosFromValue(yValue - scene->getMinY());
+ return origin + QPointF(xPos, -yPos);
+}
+
+QString ChartDataLine::createToolTipText(const DataPoint& dataPoint) const
+{
+ return scene->getXLabel() + ": <b>" + dataPoint.toolTipLabel + "</b><br/>" +
+ scene->getYLabel() + ": <b>" + QString::number(dataPoint.value) + "</b>";
+}
+
+void ChartDataLine::addLinePath(const QPainterPath& path)
+{
+ QPen pen(MarkerColor);
+ pen.setWidth(2);
+ scene->addPath(path, pen);
+}
+
+void ChartDataLine::addAreaPath(const QPainterPath& linePath)
+{
+ QPainterPath areaPath(origin);
+ areaPath.lineTo(QPointF(linePath.elementAt(0)));
+ areaPath.connectPath(linePath);
+ areaPath.lineTo(QPointF(linePath.currentPosition().x(), origin.y()));
+ scene->addPath(areaPath, QPen(FillColor), QBrush(FillColor));
+}
diff --git a/src/charts/ChartDataLine.h b/src/charts/ChartDataLine.h
new file mode 100644
index 0000000..c90ccb4
--- /dev/null
+++ b/src/charts/ChartDataLine.h
@@ -0,0 +1,37 @@
+#ifndef CHART_DATA_SET_H
+#define CHART_DATA_SET_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartScene;
+
+class ChartDataLine
+{
+public:
+ ChartDataLine(ChartScene* scene);
+ void paint();
+
+private:
+ QPointF getDataSetOrigin() const;
+ QPainterPath createLinePath();
+ QPointF getDataPos(int index, int yValue);
+ void addLinePath(const QPainterPath& path);
+ void addLineSegment(QPainterPath& path, int i);
+ void addAreaPath(const QPainterPath& linePath);
+ QString createToolTipText(const DataPoint& dataPoint) const;
+
+private:
+ static const int MarkerRadius = 2;
+ static const QColor MarkerColor;
+ static const QColor FillColor;
+
+private:
+ ChartScene* scene;
+ QPointF origin;
+ qreal xSpacing;
+};
+
+#endif
diff --git a/src/charts/ChartMarker.cpp b/src/charts/ChartMarker.cpp
new file mode 100644
index 0000000..7fdd8cd
--- /dev/null
+++ b/src/charts/ChartMarker.cpp
@@ -0,0 +1,50 @@
+#include "ChartMarker.h"
+#include "ChartToolTip.h"
+
+const QPointF ChartMarker::ToolTipOffset(45, -35);
+const QColor ChartMarker::Color(Qt::blue);
+
+ChartMarker::ChartMarker(const QPointF& center, const QString& toolTipText):
+ radius(Radius), toolTip(NULL), toolTipText(toolTipText)
+{
+ setPos(center);
+ setRect(QRectF(-HoverRadius, -HoverRadius,
+ 2 * HoverRadius, 2 * HoverRadius));
+ setAcceptHoverEvents(true);
+}
+
+ChartMarker::~ChartMarker()
+{
+ delete toolTip;
+}
+
+void ChartMarker::hoverEnterEvent(QGraphicsSceneHoverEvent*)
+{
+ radius = HighlightedRadius;
+ update();
+ if(!toolTip)
+ createToolTip();
+ toolTip->show();
+}
+
+void ChartMarker::createToolTip()
+{
+ toolTip = new ChartToolTip(mapToScene(ToolTipOffset), toolTipText);
+ scene()->addItem(toolTip);
+ toolTip->adjustPos();
+}
+
+void ChartMarker::hoverLeaveEvent(QGraphicsSceneHoverEvent*)
+{
+ radius = Radius;
+ update();
+ toolTip->hide();
+}
+
+void ChartMarker::paint(QPainter* painter, const QStyleOptionGraphicsItem*,
+ QWidget*)
+{
+ painter->setPen(Color);
+ painter->setBrush(Color);
+ painter->drawEllipse(QPointF(), radius, radius);
+}
diff --git a/src/charts/ChartMarker.h b/src/charts/ChartMarker.h
new file mode 100644
index 0000000..1f3d81e
--- /dev/null
+++ b/src/charts/ChartMarker.h
@@ -0,0 +1,36 @@
+#ifndef CHART_MARKER_H
+#define CHART_MARKER_H
+
+#include <QtWidgets>
+
+class ChartToolTip;
+
+class ChartMarker: public QGraphicsEllipseItem
+{
+public:
+ ChartMarker(const QPointF& center, const QString& toolTipText);
+ ~ChartMarker();
+ void paint(QPainter* painter,
+ const QStyleOptionGraphicsItem* option, QWidget* widget = 0);
+
+protected:
+ void hoverEnterEvent(QGraphicsSceneHoverEvent* event);
+ void hoverLeaveEvent(QGraphicsSceneHoverEvent* event);
+
+private:
+ void createToolTip();
+
+private:
+ static const QColor Color;
+ static const int HoverRadius = 15;
+ static const int Radius = 2;
+ static const int HighlightedRadius = 4;
+ static const QPointF ToolTipOffset;
+
+private:
+ int radius;
+ ChartToolTip* toolTip;
+ QString toolTipText;
+};
+
+#endif
diff --git a/src/charts/ChartScene.cpp b/src/charts/ChartScene.cpp
new file mode 100644
index 0000000..559ecc5
--- /dev/null
+++ b/src/charts/ChartScene.cpp
@@ -0,0 +1,111 @@
+#include "ChartScene.h"
+#include "ChartAxes.h"
+#include <cstdlib>
+#include <cmath>
+#include "ChartDataLine.h"
+
+const QSizeF ChartScene::Margin(30, 20);
+const QSizeF ChartScene::Padding(20, 20);
+const QSizeF ChartScene::DataSetPadding(10, 10);
+
+ChartScene::ChartScene(Chart* chart):
+ QGraphicsScene(chart), chart(chart), dataDirection(1)
+{
+ setSceneRect(0, 0, 600, 300);
+}
+
+void ChartScene::setDataSet(const QList<DataPoint>& dataSet, int xTickInterval)
+{
+ clear();
+ this->dataSet = dataSet;
+ initYScale();
+ ChartAxes(this, xTickInterval).paint();
+ ChartDataLine(this).paint();
+}
+
+void ChartScene::initYScale()
+{
+ yMin = computeMinY();
+ yMax = computeMaxY();
+ yTickSpacing = computeYTickSpacing();
+ extendYMinMax();
+ yScale = getDataSetSize().height() / getYDiff();
+}
+
+int ChartScene::computeMinY() const
+{
+ int min = 1000000;
+ foreach(DataPoint point, dataSet)
+ if(point.value < min)
+ min = point.value;
+ return min;
+}
+
+int ChartScene::computeMaxY() const
+{
+ int max = -1000000;
+ foreach(DataPoint point, dataSet)
+ if(point.value > max)
+ max = point.value;
+ return max;
+}
+
+int ChartScene::computeYTickSpacing() const
+{
+ const int higherZonesLimit = 4;
+ const int lowerZonesLimit = 3;
+
+ double order = floor(log10(getYDiff()));
+ double base = pow(10, order);
+ double zonesNum = ceil(getYDiff() / base);
+ if(zonesNum > higherZonesLimit)
+ base *= 2;
+ else if(zonesNum < lowerZonesLimit)
+ base /= 2;
+ int res = (int)base;
+ if(res == 0)
+ res = 10;
+ return res;
+}
+
+void ChartScene::extendYMinMax()
+{
+ yMin = int(floor(yMin / double(yTickSpacing))) * yTickSpacing;
+ yMax = int(ceil(yMax / double(yTickSpacing))) * yTickSpacing;
+ if (yMax == yMin)
+ yMax = yMin + yTickSpacing;
+}
+
+QSizeF ChartScene::getBorder()
+{
+ return Margin + Padding;
+}
+
+QRectF ChartScene::getChartRect() const
+{
+ QSizeF border = getBorder();
+ return sceneRect().adjusted(border.width(), border.height(),
+ -border.width(), -border.height());
+}
+
+QPointF ChartScene::getChartOrigin() const
+{
+ QRectF chartRect = getChartRect();
+ return QPointF(chartRect.left(), chartRect.bottom());
+}
+
+QSizeF ChartScene::getDataSetSize() const
+{
+ QSizeF dataSetBorder(2 * getBorder() + DataSetPadding);
+ return sceneRect().size() - dataSetBorder;
+}
+
+qreal ChartScene::getXTickSpacing() const
+{
+ return getDataSetSize().width() / (dataSet.size() - 1);
+}
+
+qreal ChartScene::getYPosFromValue(int value) const
+{
+ return yScale * value;
+}
diff --git a/src/charts/ChartScene.h b/src/charts/ChartScene.h
new file mode 100644
index 0000000..3a9af62
--- /dev/null
+++ b/src/charts/ChartScene.h
@@ -0,0 +1,55 @@
+#ifndef CHART_SCENE_H
+#define CHART_SCENE_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "Chart.h"
+#include "DataPoint.h"
+
+class ChartScene: public QGraphicsScene
+{
+public:
+ static const QSizeF Margin;
+ static const QSizeF Padding;
+ static const QSizeF DataSetPadding;
+
+public:
+ static QSizeF getBorder();
+
+public:
+ ChartScene(Chart* chart);
+ void setDataSet(const QList<DataPoint>& dataSet, int xTickInterval);
+ void setDataDirection(int direction) { dataDirection = direction; }
+ QString getXLabel() const { return chart->getXLabel(); }
+ QString getYLabel() const { return chart->getYLabel(); }
+ int getDataDirection() const { return dataDirection; }
+ QPointF getChartOrigin() const;
+ QRectF getChartRect() const;
+ QSizeF getDataSetSize() const;
+ QList<DataPoint> getDataSet() const {return dataSet; }
+ qreal getXTickSpacing() const;
+ qreal getYPosFromValue(int value) const;
+ int getMinY() const { return yMin; }
+ int getMaxY() const { return yMax; }
+ int getYDiff() const { return yMax - yMin; }
+ int getYTickSpacing() const { return yTickSpacing; }
+
+private:
+ void initYScale();
+ int computeMinY() const;
+ int computeMaxY() const;
+ int computeYTickSpacing() const;
+ void extendYMinMax();
+
+private:
+ Chart* chart;
+ QList<DataPoint> dataSet;
+ int yMin;
+ int yMax;
+ qreal yScale;
+ int yTickSpacing;
+ int dataDirection;
+};
+
+#endif
diff --git a/src/charts/ChartToolTip.cpp b/src/charts/ChartToolTip.cpp
new file mode 100644
index 0000000..15783ea
--- /dev/null
+++ b/src/charts/ChartToolTip.cpp
@@ -0,0 +1,43 @@
+#include "ChartToolTip.h"
+
+const QFont ChartToolTip::Font("Sans Serif", 11);
+
+ChartToolTip::ChartToolTip(const QPointF& pos, const QString& text):
+ text(text)
+{
+ setPos(pos);
+ document.setDefaultFont(Font);
+ document.setHtml(text);
+ QSizeF docSize = document.size();
+ setRect(QRectF(QPointF(-docSize.width() / 2, -docSize.height() / 2), docSize));
+}
+
+void ChartToolTip::paint(QPainter* painter, const QStyleOptionGraphicsItem*,
+ QWidget*)
+{
+ drawFrame(painter);
+ drawText(painter);
+}
+
+void ChartToolTip::drawFrame(QPainter* painter)
+{
+ painter->setPen(QPen(Qt::black));
+ painter->setBrush(QBrush(Qt::white));
+ painter->drawRect(rect());
+}
+
+void ChartToolTip::drawText(QPainter* painter)
+{
+ painter->translate(rect().topLeft());
+ document.drawContents(painter);
+}
+
+void ChartToolTip::adjustPos()
+{
+ QRectF toolTipRect = mapRectToScene(boundingRect());
+ QRectF sceneRect = scene()->sceneRect();
+ if(toolTipRect.right() >= sceneRect.right())
+ moveBy(-(toolTipRect.right() - sceneRect.right()) - 1, 0);
+ if(toolTipRect.top() <= sceneRect.top())
+ moveBy(0, sceneRect.top() - toolTipRect.top() + 1);
+}
diff --git a/src/charts/ChartToolTip.h b/src/charts/ChartToolTip.h
new file mode 100644
index 0000000..88453a8
--- /dev/null
+++ b/src/charts/ChartToolTip.h
@@ -0,0 +1,26 @@
+#ifndef CHART_TOOL_TIP_H
+#define CHART_TOOL_TIP_H
+
+#include <QtWidgets>
+
+class ChartToolTip: public QGraphicsRectItem
+{
+public:
+ ChartToolTip(const QPointF& pos, const QString& text);
+ void paint(QPainter* painter,
+ const QStyleOptionGraphicsItem* option, QWidget* widget = 0);
+ void adjustPos();
+
+private:
+ void drawFrame(QPainter* painter);
+ void drawText(QPainter* painter);
+
+private:
+ static const QFont Font;
+
+private:
+ QString text;
+ QTextDocument document;
+};
+
+#endif
diff --git a/src/charts/ChartView.cpp b/src/charts/ChartView.cpp
new file mode 100644
index 0000000..24a08c5
--- /dev/null
+++ b/src/charts/ChartView.cpp
@@ -0,0 +1,12 @@
+#include "ChartView.h"
+
+ChartView::ChartView(QGraphicsScene* scene):
+ QGraphicsView(scene)
+{
+ setRenderHints(QPainter::Antialiasing);
+}
+
+void ChartView::resizeEvent(QResizeEvent*)
+{
+ fitInView(scene()->sceneRect(), Qt::KeepAspectRatio);
+}
diff --git a/src/charts/ChartView.h b/src/charts/ChartView.h
new file mode 100644
index 0000000..fcd071c
--- /dev/null
+++ b/src/charts/ChartView.h
@@ -0,0 +1,16 @@
+#ifndef CHART_VIEW_H
+#define CHART_VIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class ChartView: public QGraphicsView
+{
+public:
+ ChartView(QGraphicsScene* scene);
+
+protected:
+ void resizeEvent(QResizeEvent* event);
+};
+
+#endif
diff --git a/src/charts/DataPoint.h b/src/charts/DataPoint.h
new file mode 100644
index 0000000..394d1be
--- /dev/null
+++ b/src/charts/DataPoint.h
@@ -0,0 +1,15 @@
+#ifndef DATAPOINT_H
+#define DATAPOINT_H
+
+struct DataPoint
+{
+public:
+ DataPoint(const QString& label, int value, QString toolTipLabel = ""):
+ label(label), value(value), toolTipLabel(toolTipLabel) {}
+
+ QString label;
+ int value;
+ QString toolTipLabel;
+};
+
+#endif
diff --git a/src/charts/PieChart.cpp b/src/charts/PieChart.cpp
new file mode 100644
index 0000000..0637823
--- /dev/null
+++ b/src/charts/PieChart.cpp
@@ -0,0 +1,28 @@
+#include "PieChart.h"
+#include "PieChartScene.h"
+#include "ChartView.h"
+
+PieChart::PieChart():
+ scene(new PieChartScene(this))
+{
+ createChartView();
+}
+
+void PieChart::createChartView()
+{
+ view = new ChartView(scene);
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(view);
+ mainLt->setContentsMargins(QMargins());
+ setLayout(mainLt);
+}
+
+void PieChart::setDataSet(const QList<DataPoint>& dataSet)
+{
+ scene->setDataSet(dataSet);
+}
+
+void PieChart::setColors(const QStringList& colors)
+{
+ scene->setColors(colors);
+}
diff --git a/src/charts/PieChart.h b/src/charts/PieChart.h
new file mode 100644
index 0000000..8e60833
--- /dev/null
+++ b/src/charts/PieChart.h
@@ -0,0 +1,27 @@
+#ifndef PIE_CHART_H
+#define PIE_CHART_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartView;
+class PieChartScene;
+
+class PieChart: public QWidget
+{
+public:
+ PieChart();
+ void setDataSet(const QList<DataPoint>& dataSet);
+ void setColors(const QStringList& colors);
+
+private:
+ void createChartView();
+
+protected:
+ PieChartScene* scene;
+ ChartView* view;
+};
+
+#endif
diff --git a/src/charts/PieChartScene.cpp b/src/charts/PieChartScene.cpp
new file mode 100644
index 0000000..135a0e4
--- /dev/null
+++ b/src/charts/PieChartScene.cpp
@@ -0,0 +1,30 @@
+#include <cstdlib>
+#include <cmath>
+#include "PieChartScene.h"
+#include "PieRound.h"
+#include "PieLegend.h"
+
+const QSizeF PieChartScene::Margin(20, 20);
+const QSizeF PieChartScene::Size(500, 230);
+
+PieChartScene::PieChartScene(PieChart* chart):
+ QGraphicsScene(chart), chart(chart)
+{
+ setSceneRect(0, 0, Size.width(), Size.height());
+}
+
+void PieChartScene::setDataSet(const QList<DataPoint>& dataSet)
+{
+ clear();
+ this->dataSet = dataSet;
+ int centerPos = PieRound::Margin + PieRound::Radius;
+ QPointF roundPos(QPointF(Margin.width(), Margin.height()) +
+ QPointF(centerPos, centerPos));
+ addItem(new PieRound(roundPos, this));
+ addItem(new PieLegend(roundPos + QPointF(LegendDistance, 0), this));
+}
+
+void PieChartScene::setColors(const QStringList& colors)
+{
+ this->colors = colors;
+}
diff --git a/src/charts/PieChartScene.h b/src/charts/PieChartScene.h
new file mode 100644
index 0000000..4ab8129
--- /dev/null
+++ b/src/charts/PieChartScene.h
@@ -0,0 +1,30 @@
+#ifndef PIE_CHART_SCENE_H
+#define PIE_CHART_SCENE_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+#include "PieChart.h"
+
+class PieChartScene: public QGraphicsScene
+{
+public:
+ static const QSizeF Margin;
+ static const QSizeF Size;
+ static const int LegendDistance = 230;
+
+public:
+ PieChartScene(PieChart* chart);
+ void setDataSet(const QList<DataPoint>& dataSet);
+ void setColors(const QStringList& colors);
+ QList<DataPoint> getDataSet() const {return dataSet; }
+ QStringList getColors() const { return colors; }
+
+private:
+ PieChart* chart;
+ QList<DataPoint> dataSet;
+ QStringList colors;
+};
+
+#endif
diff --git a/src/charts/PieLegend.cpp b/src/charts/PieLegend.cpp
new file mode 100644
index 0000000..5faabf4
--- /dev/null
+++ b/src/charts/PieLegend.cpp
@@ -0,0 +1,48 @@
+#include "PieLegend.h"
+#include "PieChartScene.h"
+
+PieLegend::PieLegend(const QPointF& pos, const PieChartScene* scene):
+ scene(scene)
+{
+ setPos(pos);
+ addLabels();
+}
+
+QRectF PieLegend::boundingRect() const
+{
+ return QRectF(-Width / 2, -Width / 2, Width, Width);
+}
+
+void PieLegend::addLabels()
+{
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ addLabel(i, getLabelPos(i));
+}
+
+QPointF PieLegend::getLabelPos(int index) const
+{
+ return boundingRect().topLeft() +
+ QPointF(SquareSide / 2, SquareSide / 2) +
+ index * QPointF(0, SquareSide + LabelSpacing);
+}
+
+void PieLegend::addLabel(int index, const QPointF& pos)
+{
+ QRectF squareRect(-SquareSide / 2., -SquareSide / 2.,
+ SquareSide, SquareSide);
+ QGraphicsRectItem* squareItem = new QGraphicsRectItem(squareRect, this);
+ squareItem->setBrush(QColor(scene->getColors()[index]));
+ squareItem->setPos(pos);
+
+ QGraphicsSimpleTextItem* textItem =
+ new QGraphicsSimpleTextItem(scene->getDataSet()[index].label, this);
+ textItem->setPos(getTextPos(pos, textItem));
+}
+
+QPointF PieLegend::getTextPos(const QPointF& squarePos,
+ const QGraphicsSimpleTextItem* textItem) const
+{
+ return squarePos +
+ QPointF(SquareSide / 2. + LabelTextSpacing,
+ -textItem->boundingRect().height() / 2);
+}
diff --git a/src/charts/PieLegend.h b/src/charts/PieLegend.h
new file mode 100644
index 0000000..d8e83e5
--- /dev/null
+++ b/src/charts/PieLegend.h
@@ -0,0 +1,34 @@
+#ifndef PIE_LEGEND_H
+#define PIE_LEGEND_H
+
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class PieChartScene;
+
+class PieLegend: public QGraphicsItem
+{
+public:
+ static const int Width = 150;
+ static const int SquareSide = 20;
+ static const int LabelSpacing = 10;
+ static const int LabelTextSpacing = 10;
+
+public:
+ PieLegend(const QPointF& pos, const PieChartScene* scene);
+ QRectF boundingRect() const;
+ void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget* = 0) {}
+
+private:
+ void addLabels();
+ QPointF getLabelPos(int index) const;
+ void addLabel(int index, const QPointF& pos);
+ QPointF getTextPos(const QPointF& squarePos,
+ const QGraphicsSimpleTextItem* textItem) const;
+
+private:
+ const PieChartScene* scene;
+};
+
+#endif
diff --git a/src/charts/PieRound.cpp b/src/charts/PieRound.cpp
new file mode 100644
index 0000000..f90d740
--- /dev/null
+++ b/src/charts/PieRound.cpp
@@ -0,0 +1,77 @@
+#include "PieRound.h"
+#include "PieChartScene.h"
+
+PieRound::PieRound(const QPointF& center, const PieChartScene* scene):
+ scene(scene)
+{
+ setPos(center);
+ calculateSum();
+ addSectors();
+}
+
+QRectF PieRound::boundingRect() const
+{
+ return QRectF(-Radius, -Radius, 2 * Radius, 2 * Radius);
+}
+
+void PieRound::calculateSum()
+{
+ sum = 0;
+ foreach(DataPoint point, scene->getDataSet())
+ sum += point.value;
+}
+
+void PieRound::addSectors()
+{
+ qreal angle = -90;
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ {
+ qreal sweep = 360. * scene->getDataSet()[i].value / sum;
+ addSector(i, angle, sweep);
+ angle += sweep;
+ }
+}
+
+void PieRound::addSector(int index, qreal startAngle, qreal sweep)
+{
+ addSectorItem(createSectorPath(startAngle, sweep),
+ QColor(scene->getColors()[index]));
+ addSectorLabel(index, startAngle, sweep);
+}
+
+QPainterPath PieRound::createSectorPath(qreal startAngle, qreal sweep)
+{
+ QPainterPath sectorPath;
+ sectorPath.arcTo(boundingRect(), -startAngle, -sweep);
+ sectorPath.lineTo(QPointF());
+ return sectorPath;
+}
+
+void PieRound::addSectorItem(const QPainterPath& path, QColor color)
+{
+ QGraphicsPathItem* sectorItem = new QGraphicsPathItem(path, this);
+ sectorItem->setPen(QColor("white"));
+ sectorItem->setBrush(color);
+}
+
+void PieRound::addSectorLabel(int index, qreal startAngle, qreal sweep)
+{
+ int value = scene->getDataSet()[index].value;
+ if(value == 0)
+ return;
+ QGraphicsSimpleTextItem* labelItem =
+ new QGraphicsSimpleTextItem(QString::number(value), this);
+ labelItem->setPos(getLabelPos(startAngle, sweep, labelItem));
+}
+
+QPointF PieRound::getLabelPos(qreal startAngle, qreal sweep,
+ QGraphicsSimpleTextItem* labelItem)
+{
+ int radius = sweep > 30 ? LabelRadius : 2 * LabelRadius;
+ QRectF labelArcRect(-radius, -radius, 2 * radius, 2 * radius);
+ QPainterPath path;
+ path.arcMoveTo(labelArcRect, -startAngle - sweep / 2);
+ QSizeF labelSize = labelItem->boundingRect().size();
+ return path.currentPosition() -
+ QPointF(labelSize.width() / 2, labelSize.height() / 2);
+}
diff --git a/src/charts/PieRound.h b/src/charts/PieRound.h
new file mode 100644
index 0000000..1963f26
--- /dev/null
+++ b/src/charts/PieRound.h
@@ -0,0 +1,37 @@
+#ifndef PIE_ROUND_H
+#define PIE_ROUND_H
+
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class PieChartScene;
+
+class PieRound: public QGraphicsItem
+{
+public:
+ static const int Margin = 20;
+ static const int Radius = 80;
+ static const int LabelRadius = 50;
+
+public:
+ PieRound(const QPointF& center, const PieChartScene* scene);
+ QRectF boundingRect() const;
+ void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget* = 0) {}
+
+private:
+ void calculateSum();
+ void addSectors();
+ void addSector(int index, qreal startAngle, qreal sweep);
+ QPainterPath createSectorPath(qreal startAngle, qreal sweep);
+ void addSectorItem(const QPainterPath& path, QColor color);
+ void addSectorLabel(int index, qreal startAngle, qreal sweep);
+ QPointF getLabelPos(qreal startAngle, qreal sweep,
+ QGraphicsSimpleTextItem* labelItem);
+
+private:
+ int sum;
+ const PieChartScene* scene;
+};
+
+#endif
diff --git a/src/charts/TimeChart.cpp b/src/charts/TimeChart.cpp
new file mode 100644
index 0000000..c6a60b9
--- /dev/null
+++ b/src/charts/TimeChart.cpp
@@ -0,0 +1,155 @@
+#include "TimeChart.h"
+#include "ChartScene.h"
+#include "DataPoint.h"
+
+const QList<int> TimeChart::TimeUnits = QList<int>() <<
+ 1 << 7 << 30 << 92 << 183;
+
+TimeChart::TimeChart()
+{
+}
+
+void TimeChart::setDates(const QList<QDateTime>& dates, int period,
+ int dataDirection)
+{
+ this->dates = dates;
+ scene->setDataDirection(dataDirection);
+ TimeUnit timeUnit = findTimeUnit(period);
+ QDate thisIntervalStart = getIntervalStart(QDate::currentDate(), timeUnit);
+ QList<DataPoint> dataSet;
+ int pointsNum = getDataPointsNum(period, timeUnit);
+ for(int i = 0; i < pointsNum; i++)
+ dataSet.append(createDataPoint(thisIntervalStart, timeUnit, i));
+ scene->setDataSet(dataSet, getTicksInterval(pointsNum));
+}
+
+int TimeChart::getDataPointsNum(int period, TimeUnit timeUnit)
+{
+ return period / TimeUnits.at(timeUnit);
+}
+
+int TimeChart::getTicksInterval(int pointsNum)
+{
+ int ticksInterval = 1;
+ while(pointsNum / ticksInterval > MaxXTicks)
+ ticksInterval *= 2;
+ return ticksInterval;
+}
+
+TimeChart::TimeUnit TimeChart::findTimeUnit(int period)
+{
+ for(int unit = Day; unit < (int)TimeUnitsNum; unit++)
+ if(getDataPointsNum(period, (TimeUnit)unit) <= MaxDataPoints)
+ return (TimeUnit)unit;
+ return HalfYear;
+}
+
+QDate TimeChart::getIntervalStart(const QDate& date, TimeUnit timeUnit)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ return date;
+ case Week:
+ return date.addDays(-date.dayOfWeek() + 1);
+ case Month:
+ return QDate(date.year(), date.month(), 1);
+ case Quarter:
+ return getQuarterStart(date);
+ case HalfYear:
+ return getHalfYearStart(date);
+ default:
+ return date;
+ }
+}
+
+QDate TimeChart::getQuarterStart(const QDate& date)
+{
+ const int quarterLen = 3;
+ int quarter = (date.month() - 1) / quarterLen;
+ int startMonth = quarter * quarterLen + 1;
+ return QDate(date.year(), startMonth, 1);
+}
+
+QDate TimeChart::getHalfYearStart(const QDate& date)
+{
+ const int secondHalfStart = 7;
+ int startMonth = date.month() < secondHalfStart ? 1 : secondHalfStart;
+ return QDate(date.year(), startMonth, 1);
+}
+
+QDate TimeChart::getInterval(const QDate& start, TimeUnit timeUnit,
+ int num)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ return start.addDays(num);
+ case Week:
+ return start.addDays(7 * num);
+ case Month:
+ return start.addMonths(num);
+ case Quarter:
+ return start.addMonths(3 * num);
+ case HalfYear:
+ return start.addMonths(6 * num);
+ default:
+ return start;
+ }
+}
+
+QDate TimeChart::getIntervalEnd(const QDate& start, TimeUnit timeUnit)
+{
+ return getInterval(start, timeUnit, 1).addDays(-1);
+}
+
+DataPoint TimeChart::createDataPoint(const QDate& thisIntervalStart, TimeUnit timeUnit,
+ int intervalIndex)
+{
+ QDate startDate = getInterval(thisIntervalStart, timeUnit,
+ scene->getDataDirection() * intervalIndex);
+ QDate endDate = getIntervalEnd(startDate, timeUnit);
+ QString xLabel = getDateLabel(startDate, timeUnit);
+ int value = getCardsNumForDate(startDate, endDate);
+ QString toolTipLabel = getIntervalLabel(startDate, endDate, timeUnit);
+ return DataPoint(xLabel, value, toolTipLabel);
+}
+
+QString TimeChart::getDateLabel(const QDate& date, TimeUnit timeUnit)
+{
+ return date.toString(getIntervalFormat(timeUnit));
+}
+
+QString TimeChart::getIntervalFormat(TimeUnit timeUnit)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ case Week:
+ return "dd.MM";
+ case Month:
+ case Quarter:
+ case HalfYear:
+ return "MM/yy";
+ default:
+ return "dd.MM";
+ }
+}
+
+QString TimeChart::getIntervalLabel(const QDate& startDate,
+ const QDate& endDate, TimeUnit timeUnit)
+{
+ QString startLabel = getDateLabel(startDate, timeUnit);
+ if(timeUnit == Day || timeUnit == Month)
+ return startLabel;
+ return startLabel + " - " + getDateLabel(endDate, timeUnit);
+}
+
+int TimeChart::getCardsNumForDate(const QDate& startDate, const QDate& endDate)
+{
+ int cardsNum = 0;
+ foreach(QDateTime date, dates)
+ if(date.date() >= startDate && date.date() <= endDate)
+ cardsNum++;
+ return cardsNum;
+}
diff --git a/src/charts/TimeChart.h b/src/charts/TimeChart.h
new file mode 100644
index 0000000..1d9f92d
--- /dev/null
+++ b/src/charts/TimeChart.h
@@ -0,0 +1,54 @@
+#ifndef TIME_CHART_H
+#define TIME_CHART_H
+
+#include <QtCore>
+
+#include "Chart.h"
+
+class TimeChart: public Chart
+{
+enum TimeUnit
+{
+ Day = 0,
+ Week,
+ Month,
+ Quarter,
+ HalfYear,
+ TimeUnitsNum
+};
+
+public:
+ TimeChart();
+ void setDates(const QList<QDateTime>& dates, int period,
+ int dataDirection = 1);
+
+private:
+ static TimeUnit findTimeUnit(int period);
+ static QDate getIntervalStart(const QDate& date, TimeUnit timeUnit);
+ static QDate getInterval(const QDate& start, TimeUnit timeUnit,
+ int num);
+ static QDate getIntervalEnd(const QDate& start, TimeUnit timeUnit);
+ static QString getDateLabel(const QDate& date, TimeUnit timeUnit);
+ static QString getIntervalFormat(TimeUnit timeUnit);
+ static QString getIntervalLabel(const QDate& startDate,
+ const QDate& endDate, TimeUnit timeUnit);
+ static QDate getQuarterStart(const QDate& date);
+ static QDate getHalfYearStart(const QDate& date);
+ static int getDataPointsNum(int period, TimeUnit timeUnit);
+ static int getTicksInterval(int pointsNum);
+
+private:
+ DataPoint createDataPoint(const QDate& thisIntervalStart, TimeUnit timeUnit,
+ int intervalIndex);
+ int getCardsNumForDate(const QDate& startDate, const QDate& endDate);
+
+private:
+ static const QList<int> TimeUnits;
+ static const int MaxDataPoints = 28;
+ static const int MaxXTicks = 12;
+
+private:
+ QList<QDateTime> dates;
+};
+
+#endif
diff --git a/src/dic-options/DictionaryOptionsDialog.cpp b/src/dic-options/DictionaryOptionsDialog.cpp
new file mode 100644
index 0000000..46bb245
--- /dev/null
+++ b/src/dic-options/DictionaryOptionsDialog.cpp
@@ -0,0 +1,62 @@
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/CardPack.h"
+#include "FieldsPage.h"
+#include "PacksPage.h"
+#include "FieldsListModel.h"
+
+DictionaryOptionsDialog::DictionaryOptionsDialog( Dictionary* aDict, QWidget* aParent ):
+ QDialog( aParent ), m_origDictPtr( aDict )
+ {
+ initData();
+ createPages();
+
+ QDialogButtonBox* okCancelBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
+ Qt::Horizontal );
+ connect( okCancelBox, SIGNAL(accepted()), this, SLOT(accept()) );
+ connect( okCancelBox, SIGNAL(rejected()), this, SLOT(reject()) );
+
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->addWidget(createDictPathLabel());
+ mainLayout->addWidget( m_pages );
+ mainLayout->addWidget( okCancelBox );
+ setLayout( mainLayout );
+
+ setWindowTitle( tr("Dictionary options") );
+ }
+
+DictionaryOptionsDialog::~DictionaryOptionsDialog()
+{
+ delete m_emptyField;
+}
+
+QSize DictionaryOptionsDialog::sizeHint() const
+{
+ return QSize(700, 400);
+}
+
+QLabel* DictionaryOptionsDialog::createDictPathLabel()
+{
+ const int MaxPathLength = 500;
+ QLabel* dictPathLabel = new QLabel;
+ QFontMetrics metrics(dictPathLabel->font());
+ QString dictPath = QDir::toNativeSeparators(m_origDictPtr->getFilePath());
+ dictPath = metrics.elidedText(dictPath, Qt::ElideMiddle, MaxPathLength);
+ dictPathLabel->setText("<b>" + tr("File name") + "</b>: " + dictPath);
+ return dictPathLabel;
+}
+
+void DictionaryOptionsDialog::initData()
+ {
+ m_dict.setDictConfig( m_origDictPtr );
+ m_emptyField = new Field();
+ m_fieldsListModel = new FieldsListModel( this );
+ }
+
+void DictionaryOptionsDialog::createPages()
+ {
+ m_pages = new QTabWidget;
+ m_pages->addTab( new FieldsPage( this ), QIcon(":/images/fields.png"), tr("Fields") );
+ m_pages->addTab( new PacksPage( this ), QIcon(":/images/word-drill.png"), tr("Card packs") );
+ }
+
+
diff --git a/src/dic-options/DictionaryOptionsDialog.h b/src/dic-options/DictionaryOptionsDialog.h
new file mode 100644
index 0000000..3a57cb6
--- /dev/null
+++ b/src/dic-options/DictionaryOptionsDialog.h
@@ -0,0 +1,41 @@
+#ifndef DICTIONARYOPTIONSDIALOG_H
+#define DICTIONARYOPTIONSDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "../dictionary/Field.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Dictionary.h"
+
+class Dictionary;
+class FieldsListModel;
+
+class DictionaryOptionsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ DictionaryOptionsDialog( Dictionary* aDict, QWidget* aParent );
+ ~DictionaryOptionsDialog();
+ QSize sizeHint() const;
+
+private:
+ void initData();
+ void createPages();
+ QLabel* createDictPathLabel();
+
+public:
+ FieldsListModel* m_fieldsListModel;
+ Field* m_emptyField;
+ Dictionary m_dict;
+
+private:
+ Dictionary* m_origDictPtr;
+ QListWidget* m_contentsWidget;
+ QTabWidget* m_pages;
+};
+
+Q_DECLARE_METATYPE(QList<QPersistentModelIndex>)
+
+#endif // DICTIONARYOPTIONSDIALOG_H
diff --git a/src/dic-options/DraggableListModel.cpp b/src/dic-options/DraggableListModel.cpp
new file mode 100644
index 0000000..2ea0523
--- /dev/null
+++ b/src/dic-options/DraggableListModel.cpp
@@ -0,0 +1,83 @@
+#include "DraggableListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+Qt::ItemFlags DraggableListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsDragEnabled | defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+ }
+
+Qt::DropActions DraggableListModel::supportedDropActions() const
+ {
+ return Qt::MoveAction;
+ }
+
+QStringList DraggableListModel::mimeTypes() const
+ {
+ QStringList types;
+ types << "application/octet-stream";
+ return types;
+ }
+
+QMimeData *DraggableListModel::mimeData(const QModelIndexList &indexes) const
+ {
+ QStringList list;
+ QModelIndexList validIndexes;
+ foreach (QModelIndex index, indexes)
+ if (index.isValid() && index.column() == 0)
+ validIndexes << index;
+ qSort(validIndexes);
+
+ int num = validIndexes.size();
+ QByteArray encodedData;
+ QDataStream stream( &encodedData, QIODevice::WriteOnly );
+ const void** ptrs = new const void*[num];
+ int i=0;
+ foreach (QModelIndex index, validIndexes)
+ ptrs[i++] = dataPtr( index );
+ stream.writeBytes( (char*)ptrs, num*sizeof(void*) );
+ delete ptrs;
+ QMimeData *mimeData = new QMimeData();
+ mimeData->setData( "application/octet-stream", encodedData );
+ return mimeData;
+ }
+
+bool DraggableListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int /*column*/, const QModelIndex &parent)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+
+ int beginRow;
+ if (row != -1)
+ beginRow = row;
+ else if (parent.isValid())
+ beginRow = parent.row();
+ else
+ beginRow = rowCount(QModelIndex());
+
+ QByteArray encodedData = data->data("application/octet-stream");
+ QDataStream stream( &encodedData, QIODevice::ReadOnly );
+ void** ptrs = new void*[ rowCount() ];
+ uint num;
+ stream.readBytes( (char*&)ptrs, num ); //TODO: Avoid converting pointer to other types
+ num /= sizeof(void*);
+
+ QList<QPersistentModelIndex> movedIndexes;
+ for(uint i=0; i<num; i++)
+ {
+ insertPointer( beginRow + i, ptrs[i] );
+ movedIndexes << QPersistentModelIndex( index(beginRow + i, 0, QModelIndex()) );
+ }
+ delete ptrs;
+ emit indexesDropped( movedIndexes );
+ return true;
+ }
diff --git a/src/dic-options/DraggableListModel.h b/src/dic-options/DraggableListModel.h
new file mode 100644
index 0000000..e323def
--- /dev/null
+++ b/src/dic-options/DraggableListModel.h
@@ -0,0 +1,33 @@
+#ifndef DRAGGABLELISTMODEL_H
+#define DRAGGABLELISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class DraggableListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ DraggableListModel( DictionaryOptionsDialog* aParent ):
+ QAbstractListModel( aParent ), m_parent( aParent )
+ {}
+
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ Qt::DropActions supportedDropActions() const;
+ QStringList mimeTypes() const;
+ QMimeData * mimeData(const QModelIndexList &indexes) const;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+ virtual const void* dataPtr( const QModelIndex &aIndex ) const = 0;
+ virtual void insertPointer(int aPos, void *aData) = 0;
+
+signals:
+ void indexesDropped(QList<QPersistentModelIndex> aIndexes);
+
+protected:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif
diff --git a/src/dic-options/FieldStyleDelegate.cpp b/src/dic-options/FieldStyleDelegate.cpp
new file mode 100644
index 0000000..6efee8c
--- /dev/null
+++ b/src/dic-options/FieldStyleDelegate.cpp
@@ -0,0 +1,35 @@
+#include "FieldStyleDelegate.h"
+#include "../dictionary/Field.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QComboBox>
+
+FieldStyleDelegate::FieldStyleDelegate(QObject *parent) :
+ QStyledItemDelegate(parent)
+ {
+ }
+
+QWidget* FieldStyleDelegate::createEditor(QWidget *parent,
+ const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const
+ {
+ QComboBox* comboBox = new QComboBox( parent );
+ comboBox->insertItems( 0, FieldStyleFactory::inst()->getStyleNames() );
+ return comboBox;
+ }
+
+void FieldStyleDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+ {
+ QString value = index.model()->data(index, Qt::EditRole).toString();
+ int cbIndex = FieldStyleFactory::inst()->getStyleNames().indexOf( value );
+ QComboBox* comboBox = static_cast<QComboBox*>(editor);
+ comboBox->setCurrentIndex( cbIndex );
+ }
+
+void FieldStyleDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
+ const QModelIndex &index) const
+ {
+ QComboBox* comboBox = static_cast<QComboBox*>(editor);
+ QString value = comboBox->currentText();
+ model->setData(index, value, Qt::EditRole);
+ }
+
diff --git a/src/dic-options/FieldStyleDelegate.h b/src/dic-options/FieldStyleDelegate.h
new file mode 100644
index 0000000..2d969f5
--- /dev/null
+++ b/src/dic-options/FieldStyleDelegate.h
@@ -0,0 +1,19 @@
+#ifndef FIELDSTYLEDELEGATE_H
+#define FIELDSTYLEDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+class FieldStyleDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ FieldStyleDelegate(QObject *parent = 0);
+ QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const;
+ void setEditorData(QWidget *editor, const QModelIndex &index) const;
+ void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;
+};
+
+
+#endif // FIELDSTYLEDELEGATE_H
diff --git a/src/dic-options/FieldsListModel.cpp b/src/dic-options/FieldsListModel.cpp
new file mode 100644
index 0000000..9fd23ac
--- /dev/null
+++ b/src/dic-options/FieldsListModel.cpp
@@ -0,0 +1,222 @@
+#include "FieldsListModel.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+QVariant FieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ const Field* field = m_parent->m_dict.field( index.row() );
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ switch( index.column() )
+ {
+ case 0:
+ return field->name();
+ case 1:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ case Qt::EditRole:
+ switch( index.column() )
+ {
+ case 0:
+ return field->name();
+ case 1:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ case EStyleRole:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ }
+
+QVariant FieldsListModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+ {
+ if( role != Qt::DisplayRole )
+ return QVariant();
+ if( orientation == Qt::Horizontal )
+ switch( section )
+ {
+ case 0:
+ return tr("Field");
+ case 1:
+ return tr("Style");
+ default:
+ return QVariant();
+ }
+ else
+ return QVariant();
+ }
+
+Qt::ItemFlags FieldsListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsEditable | Qt::ItemIsDragEnabled | defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+ }
+
+bool FieldsListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+ {
+ if( !index.isValid() || role!=Qt::EditRole )
+ return false;
+ switch( index.column() )
+ {
+ case 0:
+ m_parent->m_dict.setFieldName(index.row(), value.toString());
+ emit dataChanged(index, index);
+ return true;
+ case 1:
+ m_parent->m_dict.setFieldStyle(index.row(), value.toString());
+ emit dataChanged(index, index);
+ return true;
+ default:
+ return true;
+ }
+ }
+
+bool FieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.insertField( position, tr("new field") );
+ endInsertRows();
+ return true;
+ }
+
+bool FieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.removeField( position );
+ endRemoveRows();
+ return true;
+ }
+
+void FieldsListModel::removeField(int aPos )
+ {
+ beginRemoveRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.destroyField( aPos );
+ endRemoveRows();
+ }
+
+void FieldsListModel::insertField(int aPos, Field* aField )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.insertField( aPos, aField );
+ endInsertRows();
+ }
+
+Qt::DropActions FieldsListModel::supportedDropActions() const
+ {
+ return Qt::MoveAction;
+ }
+
+QStringList FieldsListModel::mimeTypes() const
+ {
+ QStringList types;
+ types << "application/octet-stream";
+ return types;
+ }
+
+QMimeData *FieldsListModel::mimeData(const QModelIndexList &indexes) const
+ {
+ QStringList list;
+ QModelIndexList validIndexes;
+ foreach (QModelIndex index, indexes)
+ if (index.isValid() && index.column() == 0)
+ validIndexes << index;
+ qSort(validIndexes);
+
+ int num = validIndexes.size();
+ QByteArray encodedData;
+ QDataStream stream( &encodedData, QIODevice::WriteOnly );
+ const Field** fieldPtrs = new const Field*[num];
+ int i=0;
+ foreach (QModelIndex index, validIndexes)
+ fieldPtrs[i++] = m_parent->m_dict.field( index.row() );
+ stream.writeBytes( (char*)fieldPtrs, num*sizeof(Field*) );
+ delete fieldPtrs;
+ QMimeData *mimeData = new QMimeData();
+ mimeData->setData( "application/octet-stream", encodedData );
+ return mimeData;
+ }
+
+bool FieldsListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int column, const QModelIndex &parent)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+ if (column > 0)
+ column = 0;
+
+ int beginRow;
+ if (row != -1)
+ beginRow = row;
+ else if (parent.isValid())
+ beginRow = parent.row();
+ else
+ beginRow = rowCount(QModelIndex());
+
+ QByteArray encodedData = data->data("application/octet-stream");
+ QDataStream stream( &encodedData, QIODevice::ReadOnly );
+ Field** fieldPtrs = new Field*[ m_parent->m_dict.fieldsNum() ];
+ uint num;
+ stream.readBytes( (char*&)fieldPtrs, num ); //TODO: Avoid converting pointer to other types
+ num /= sizeof(Field*);
+
+ QList<QPersistentModelIndex> movedIndexes;
+ for(uint i=0; i<num; i++)
+ {
+ insertField( beginRow + i, fieldPtrs[i] );
+ movedIndexes << QPersistentModelIndex( index(beginRow + i, 0, QModelIndex()) );
+ }
+ delete fieldPtrs;
+ emit indexesDropped( movedIndexes );
+ return true;
+ }
+
+void FieldsListModel::moveIndexesUpDown( QModelIndexList aIndexes, int aDirection )
+ {
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, aIndexes)
+ if( idx.isValid() && idx.column() == 0 )
+ pSrcIndexes << QPersistentModelIndex( idx );
+
+ int rowsNum = pSrcIndexes.size();
+ if( rowsNum == 0 )
+ return;
+
+ qSort( pSrcIndexes );
+
+ int beginRow;
+ if( aDirection < 0 )
+ beginRow = pSrcIndexes[0].row()-1;
+ else
+ beginRow = pSrcIndexes[rowsNum-1].row()+2;
+ if( rowsNum == 1 && (beginRow < 0 || beginRow > rowCount()) )
+ return;
+ if( beginRow < 0 )
+ beginRow = 0;
+ if( beginRow > rowCount() )
+ beginRow = rowCount();
+
+ QMimeData* mime = mimeData( aIndexes );
+ dropMimeData( mime, Qt::MoveAction, beginRow, 0, QModelIndex() );
+ foreach (QModelIndex index, pSrcIndexes)
+ removeRow( index.row() );
+ }
diff --git a/src/dic-options/FieldsListModel.h b/src/dic-options/FieldsListModel.h
new file mode 100644
index 0000000..1cc4534
--- /dev/null
+++ b/src/dic-options/FieldsListModel.h
@@ -0,0 +1,49 @@
+#ifndef FIELDSLISTMODEL_H
+#define FIELDSLISTMODEL_H
+
+#include <QAbstractTableModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class FieldsListModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum
+ {
+ EStyleRole = Qt::UserRole
+ };
+
+ FieldsListModel( DictionaryOptionsDialog* aParent ):
+ QAbstractTableModel( aParent ), m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.fieldsNum(); }
+ int columnCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return 2; }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
+ bool insertRows(int position, int rows, const QModelIndex &/*parent*/);
+ bool removeRows(int position, int rows, const QModelIndex &/*parent*/);
+ void insertField(int aPos, Field *aField);
+ void removeField( int aPos );
+ Qt::DropActions supportedDropActions() const;
+ QStringList mimeTypes() const;
+ QMimeData* mimeData(const QModelIndexList &indexes) const;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent);
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ void moveIndexesUpDown(QModelIndexList aIndexes, int aDirection);
+
+signals:
+ void indexesDropped(QList<QPersistentModelIndex> aIndexes);
+
+private:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif // FIELDSLISTMODEL_H
diff --git a/src/dic-options/FieldsPage.cpp b/src/dic-options/FieldsPage.cpp
new file mode 100644
index 0000000..ea9a741
--- /dev/null
+++ b/src/dic-options/FieldsPage.cpp
@@ -0,0 +1,139 @@
+#include "FieldsPage.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/CardPack.h"
+#include "FieldsListModel.h"
+#include "FieldsPreviewModel.h"
+
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <QToolButton>
+#include <QGroupBox>
+
+FieldsPage::FieldsPage( DictionaryOptionsDialog* aParent ):
+ QWidget( aParent ), m_parent( aParent )
+ {
+ createFieldsList();
+ createFieldsPreview();
+
+ QHBoxLayout* mainLt = new QHBoxLayout;
+ mainLt->addLayout( m_fieldsLt );
+ mainLt->addWidget( m_previewGroup );
+ setLayout( mainLt );
+ }
+
+void FieldsPage::createFieldsList()
+ {
+ QGroupBox* fieldsGroup = new QGroupBox( tr("Fields") );
+
+ QToolButton* moveUpBtn = new QToolButton;
+ moveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ moveUpBtn->setObjectName("up");
+ moveUpBtn->setToolTip(tr("Move up"));
+ QToolButton* moveDownBtn = new QToolButton;
+ moveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ moveDownBtn->setObjectName("down");
+ moveDownBtn->setToolTip(tr("Move down"));
+ QVBoxLayout* upDownLt = new QVBoxLayout;
+ upDownLt->addWidget( moveUpBtn );
+ upDownLt->addWidget( moveDownBtn );
+ upDownLt->addStretch( 1 );
+
+ connect( moveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( moveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_fieldsListView = new FieldsView;
+ m_fieldsListView->setModel( m_parent->m_fieldsListModel );
+ QHBoxLayout* listHLt = new QHBoxLayout;
+ listHLt->addWidget( m_fieldsListView );
+ listHLt->addLayout( upDownLt );
+ fieldsGroup->setLayout( listHLt );
+
+ QPushButton* addBtn = new QPushButton( QPixmap(":/images/add.png"), tr("Add") );
+ addBtn->setToolTip(tr("Add new field"));
+ QPushButton* removeBtn = new QPushButton( QPixmap(":/images/delete.png"), tr("Remove") );
+ removeBtn->setToolTip(tr("Remove field(s)"));
+ QPushButton* editBtn = new QPushButton( QPixmap(":/images/edit.png"), tr("Rename") );
+ editBtn->setToolTip(tr("Rename field"));
+ QHBoxLayout* btnLt = new QHBoxLayout;
+ btnLt->addWidget( addBtn );
+ btnLt->addWidget( removeBtn );
+ btnLt->addWidget( editBtn );
+
+ connect( addBtn, SIGNAL(clicked()), this, SLOT(addRow()) );
+ connect( removeBtn, SIGNAL(clicked()), this, SLOT(removeRows()) );
+ connect( editBtn, SIGNAL(clicked()), this, SLOT(editField()) );
+
+ m_fieldsLt = new QVBoxLayout;
+ m_fieldsLt->addWidget( fieldsGroup );
+ m_fieldsLt->addLayout( btnLt );
+ }
+
+void FieldsPage::createFieldsPreview()
+ {
+ FieldsPreviewModel* fieldsPreviewModel = new FieldsPreviewModel( m_parent );
+ m_fieldsPreview = new QListView;
+ m_fieldsPreview->setModel( fieldsPreviewModel );
+
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ m_fieldsPreview, SLOT(reset()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_fieldsPreview, SLOT(reset()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_fieldsPreview, SLOT(reset()) );
+
+ QHBoxLayout* previewLt = new QHBoxLayout;
+ previewLt->addWidget( m_fieldsPreview );
+ m_previewGroup = new QGroupBox(tr("Preview"));
+ m_previewGroup->setLayout( previewLt );
+ }
+
+void FieldsPage::moveItemsUpDown()
+ {
+ int direction;
+ if( sender()->objectName() == "up" )
+ direction = -1;
+ else
+ direction = 1;
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedIndexes();
+ m_fieldsListView->model()->moveIndexesUpDown( selectedIndexes, direction );
+ }
+
+void FieldsPage::addRow()
+ {
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedRows();
+ int row;
+ if( selectedIndexes.size() > 0 )
+ {
+ qSort(selectedIndexes);
+ row = selectedIndexes[selectedIndexes.size()-1].row() + 1;
+ }
+ else
+ row = 0;
+ m_fieldsListView->model()->insertRow( row, QModelIndex() );
+ }
+
+void FieldsPage::removeRows()
+ {
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedRows();
+ QList<QPersistentModelIndex> pIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pIndexes << QPersistentModelIndex( idx );
+ if( pIndexes.size() == 0 )
+ return;
+ foreach( QModelIndex idx, pIndexes )
+ m_fieldsListView->model()->removeField( idx.row() );
+ }
+
+void FieldsPage::editField()
+ {
+ QModelIndex idx = m_fieldsListView->currentIndex();
+ if( !idx.isValid() )
+ return;
+ m_fieldsListView->edit( idx );
+ }
diff --git a/src/dic-options/FieldsPage.h b/src/dic-options/FieldsPage.h
new file mode 100644
index 0000000..3cfdf99
--- /dev/null
+++ b/src/dic-options/FieldsPage.h
@@ -0,0 +1,37 @@
+#ifndef FIELDSPAGE_H
+#define FIELDSPAGE_H
+
+#include <QListView>
+
+#include "DictionaryOptionsDialog.h"
+#include "FieldsView.h"
+
+class Dictionary;
+class QGroupBox;
+class QVBoxLayout;
+
+class FieldsPage : public QWidget
+{
+ Q_OBJECT
+public:
+ FieldsPage( DictionaryOptionsDialog* aParent );
+
+private slots:
+ void moveItemsUpDown();
+ void addRow();
+ void removeRows();
+ void editField();
+
+private:
+ void createFieldsList();
+ void createFieldsPreview();
+
+private:
+ DictionaryOptionsDialog* m_parent;
+ QVBoxLayout* m_fieldsLt;
+ QGroupBox* m_previewGroup;
+ FieldsView* m_fieldsListView;
+ QListView* m_fieldsPreview;
+};
+
+#endif
diff --git a/src/dic-options/FieldsPreviewModel.cpp b/src/dic-options/FieldsPreviewModel.cpp
new file mode 100644
index 0000000..e6ce1c6
--- /dev/null
+++ b/src/dic-options/FieldsPreviewModel.cpp
@@ -0,0 +1,34 @@
+#include "FieldsPreviewModel.h"
+#include "../dictionary/Field.h"
+#include "../field-styles/FieldStyle.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QBrush>
+
+QVariant FieldsPreviewModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ Field* field = m_parent->m_dict.fields().value(index.row());
+ const FieldStyle fieldStyle = FieldStyleFactory::inst()->getStyle(field->style());
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return fieldStyle.prefix + field->name() + fieldStyle.suffix;
+ case Qt::FontRole:
+ return fieldStyle.font;
+ case Qt::BackgroundRole:
+ return QBrush( FieldStyleFactory::inst()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.color;
+ case Qt::SizeHintRole:
+ return QSize(0, 50);
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ }
diff --git a/src/dic-options/FieldsPreviewModel.h b/src/dic-options/FieldsPreviewModel.h
new file mode 100644
index 0000000..38e1fe2
--- /dev/null
+++ b/src/dic-options/FieldsPreviewModel.h
@@ -0,0 +1,27 @@
+#ifndef FIELDSPREVIEWMODEL_H
+#define FIELDSPREVIEWMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class FieldsPreviewModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ FieldsPreviewModel( DictionaryOptionsDialog* aParent ):
+ QAbstractListModel( aParent ), m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.fieldsNum(); }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ Qt::ItemFlags flags(const QModelIndex &/*index*/) const {return Qt::NoItemFlags;}
+
+private:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif // FIELDSPREVIEWMODEL_H
diff --git a/src/dic-options/FieldsView.cpp b/src/dic-options/FieldsView.cpp
new file mode 100644
index 0000000..65801c7
--- /dev/null
+++ b/src/dic-options/FieldsView.cpp
@@ -0,0 +1,41 @@
+#include "FieldsView.h"
+#include "FieldStyleDelegate.h"
+
+#include <QHeaderView>
+
+FieldsView::FieldsView(QWidget *parent):
+ QTableView( parent )
+ {
+ setDragEnabled(true);
+ setAcceptDrops(true);
+ setDropIndicatorShown(true);
+ setDragDropOverwriteMode( false );
+ FieldStyleDelegate* delegate = new FieldStyleDelegate(this);
+ setItemDelegateForColumn( 1, delegate );
+
+ setShowGrid(false);
+ verticalHeader()->hide();
+ setSelectionBehavior( QAbstractItemView::SelectRows );
+ }
+
+void FieldsView::startDrag(Qt::DropActions supportedActions)
+ {
+ selectionModel()->select( selectionModel()->selection(),
+ QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ QAbstractItemView::startDrag( supportedActions );
+ }
+
+void FieldsView::setModel( FieldsListModel* aModel )
+ {
+ QTableView::setModel( aModel );
+ qRegisterMetaType< QList<QPersistentModelIndex> >();
+ connect( aModel, SIGNAL(indexesDropped(QList<QPersistentModelIndex>)),
+ this, SLOT(selectIndexes(QList<QPersistentModelIndex>)), Qt::QueuedConnection );
+ }
+
+void FieldsView::selectIndexes( QList<QPersistentModelIndex> aIndexes )
+ {
+ selectionModel()->clearSelection();
+ foreach( QPersistentModelIndex pIndex, aIndexes )
+ selectionModel()->select( pIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ }
diff --git a/src/dic-options/FieldsView.h b/src/dic-options/FieldsView.h
new file mode 100644
index 0000000..6944474
--- /dev/null
+++ b/src/dic-options/FieldsView.h
@@ -0,0 +1,24 @@
+#ifndef FIELDSVIEW_H
+#define FIELDSVIEW_H
+
+#include <QTableView>
+
+#include "FieldsListModel.h"
+
+class FieldsView : public QTableView
+{
+ Q_OBJECT
+public:
+ FieldsView(QWidget *parent = 0);
+
+ FieldsListModel* model() const
+ { return qobject_cast<FieldsListModel*>(QAbstractItemView::model()); }
+
+ void startDrag(Qt::DropActions supportedActions);
+ void setModel(FieldsListModel *aModel);
+public slots:
+ void selectIndexes(QList<QPersistentModelIndex> aIndexes);
+
+};
+
+#endif // FIELDSVIEW_H
diff --git a/src/dic-options/PackFieldsListModel.cpp b/src/dic-options/PackFieldsListModel.cpp
new file mode 100644
index 0000000..eb6eb6f
--- /dev/null
+++ b/src/dic-options/PackFieldsListModel.cpp
@@ -0,0 +1,101 @@
+#include "PackFieldsListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+int PackFieldsListModel::rowCount( const QModelIndex& /*parent*/ ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return 0;
+ return pack->getFields().size();
+ }
+
+QVariant PackFieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return QVariant();
+ const Field* field = pack->getFields().value(index.row());
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return field->name();
+ case Qt::FontRole:
+ if( index.row() == 0 )
+ {
+ QFont font;
+ font.setBold(true);
+ return font;
+ }
+ else
+ return QFont();
+ default:
+ return QVariant();
+ }
+ }
+
+void PackFieldsListModel::changeParentRow( const QModelIndex& aIndex )
+ {
+ m_parentRow = aIndex.row();
+ emit layoutChanged();
+ }
+
+bool PackFieldsListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ const Field* field = m_parent->m_dict.field( aValue.toString() );
+ if( !field )
+ return false;
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ pack->setField( index.row(), field );
+ emit dataChanged(index, index);
+ return true;
+ }
+
+bool PackFieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ for (int row = 0; row < rows; ++row)
+ pack->insertField( position, m_parent->m_emptyField );
+ endInsertRows();
+ return true;
+ }
+
+bool PackFieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ for (int row = 0; row < rows; ++row)
+ pack->removeField( position );
+ endRemoveRows();
+ return true;
+ }
+
+const void* PackFieldsListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return NULL;
+ const Field* field = pack->getFields().value( aIndex.row() );
+ return static_cast<const void*>( field );
+ }
+
+void PackFieldsListModel::insertPointer( int aPos, void* aData )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos );
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return;
+ pack->insertField( aPos, static_cast<Field*>( aData ) );
+ endInsertRows();
+ }
diff --git a/src/dic-options/PackFieldsListModel.h b/src/dic-options/PackFieldsListModel.h
new file mode 100644
index 0000000..06565c5
--- /dev/null
+++ b/src/dic-options/PackFieldsListModel.h
@@ -0,0 +1,34 @@
+#ifndef PACKFIELDSLISTMODEL_H
+#define PACKFIELDSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+#include "../dictionary/Dictionary.h"
+
+class PackFieldsListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ PackFieldsListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent ), m_parentRow( 0 )
+ {}
+
+ int rowCount( const QModelIndex& parent = QModelIndex() ) const;
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+
+public slots:
+ void changeParentRow( const QModelIndex& aIndex );
+
+private:
+ int m_parentRow;
+};
+
+#endif
diff --git a/src/dic-options/PackFieldsView.cpp b/src/dic-options/PackFieldsView.cpp
new file mode 100644
index 0000000..6925984
--- /dev/null
+++ b/src/dic-options/PackFieldsView.cpp
@@ -0,0 +1,36 @@
+#include "PackFieldsView.h"
+
+PackFieldsView::PackFieldsView(QWidget *parent):
+ QListView( parent )
+ {
+ setDragEnabled(true);
+ setAcceptDrops(true);
+ setDropIndicatorShown(true);
+ setDragDropOverwriteMode( false );
+ }
+
+void PackFieldsView::setModel( QAbstractItemModel* aModel )
+ {
+ QListView::setModel( aModel );
+ qRegisterMetaType< QList<QPersistentModelIndex> >();
+ connect( aModel, SIGNAL(indexesDropped(QList<QPersistentModelIndex>)),
+ this, SLOT(selectIndexes(QList<QPersistentModelIndex>)), Qt::QueuedConnection );
+ }
+
+void PackFieldsView::selectIndexes( QList<QPersistentModelIndex> aIndexes )
+ {
+ selectionModel()->clearSelection();
+ foreach( QPersistentModelIndex pIndex, aIndexes )
+ {
+ QModelIndex index = model()->index( pIndex.row(), 0, pIndex.parent() );
+ selectionModel()->select( index, QItemSelectionModel::Select );
+ }
+ selectionModel()->setCurrentIndex( aIndexes[0], QItemSelectionModel::NoUpdate );
+ }
+
+void PackFieldsView::updateCurrent()
+ {
+ QItemSelection selection = selectionModel()->selection();
+ reset();
+ selectionModel()->select( selection, QItemSelectionModel::Select );
+ }
diff --git a/src/dic-options/PackFieldsView.h b/src/dic-options/PackFieldsView.h
new file mode 100644
index 0000000..7c4aeea
--- /dev/null
+++ b/src/dic-options/PackFieldsView.h
@@ -0,0 +1,24 @@
+#ifndef PACKFIELDSVIEW_H
+#define PACKFIELDSVIEW_H
+
+#include <QListView>
+
+#include "PackFieldsListModel.h"
+#include "DraggableListModel.h"
+
+class PackFieldsView : public QListView
+{
+ Q_OBJECT
+public:
+ PackFieldsView(QWidget *parent = 0);
+
+ void setModel(QAbstractItemModel *aModel);
+ DraggableListModel* model() const
+ { return qobject_cast<DraggableListModel*>(QAbstractItemView::model()); }
+public slots:
+ void selectIndexes(QList<QPersistentModelIndex> aIndexes);
+ void updateCurrent();
+
+};
+
+#endif
diff --git a/src/dic-options/PacksListModel.cpp b/src/dic-options/PacksListModel.cpp
new file mode 100644
index 0000000..c2dfc31
--- /dev/null
+++ b/src/dic-options/PacksListModel.cpp
@@ -0,0 +1,81 @@
+#include "PacksListModel.h"
+#include "../dictionary/CardPack.h"
+
+QVariant PacksListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ {
+ const CardPack* pack = m_parent->m_dict.cardPack( index.row() );
+ if( !pack )
+ return QVariant();
+ return pack->id();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+bool PacksListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ m_parent->m_dict.cardPack( index.row() )->setName( aValue.toString() );
+ emit dataChanged(index, index);
+ return true;
+ }
+
+Qt::ItemFlags PacksListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = DraggableListModel::flags(index);
+ return defaultFlags;
+ }
+
+bool PacksListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int i = 0; i < rows; i++)
+ {
+ CardPack* newPack = new CardPack( &(m_parent->m_dict) );
+ newPack->addField( m_parent->m_dict.field(0) );
+ m_parent->m_dict.insertPack( position + i, newPack );
+ }
+ endInsertRows();
+ return true;
+ }
+
+bool PacksListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.removePack( position );
+ endRemoveRows();
+ return true;
+ }
+
+const void* PacksListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( aIndex.row() );
+ return static_cast<const void*>( pack );
+ }
+
+void PacksListModel::insertPointer( int aPos, void* aData )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos );
+ m_parent->m_dict.insertPack( aPos, static_cast<CardPack*>( aData ) );
+ endInsertRows();
+ }
+
+void PacksListModel::removePack( int aPos )
+ {
+ beginRemoveRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.destroyPack( aPos );
+ endRemoveRows();
+ }
diff --git a/src/dic-options/PacksListModel.h b/src/dic-options/PacksListModel.h
new file mode 100644
index 0000000..3523912
--- /dev/null
+++ b/src/dic-options/PacksListModel.h
@@ -0,0 +1,32 @@
+#ifndef PACKSLISTMODEL_H
+#define PACKSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+#include "../dictionary/Dictionary.h"
+
+class PacksListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ PacksListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.cardPacksNum(); }
+
+ QVariant data( const QModelIndex &index, int role ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+ void removePack( int aPos );
+};
+
+#endif
diff --git a/src/dic-options/PacksPage.cpp b/src/dic-options/PacksPage.cpp
new file mode 100644
index 0000000..e78bbf0
--- /dev/null
+++ b/src/dic-options/PacksPage.cpp
@@ -0,0 +1,345 @@
+#include "PacksPage.h"
+#include "FieldsListModel.h"
+#include "PacksListModel.h"
+#include "PackFieldsListModel.h"
+#include "PackFieldsView.h"
+#include "UnusedFieldsListModel.h"
+#include "../dictionary/Dictionary.h"
+#include "../study/CardSideView.h"
+#include "../dictionary/CardPack.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QLabel>
+#include <QPushButton>
+#include <QToolButton>
+#include <QStringListModel>
+
+PacksPage::PacksPage( DictionaryOptionsDialog* aParent ):
+ QWidget( aParent ), m_parent( aParent )
+ {
+ createPacksList();
+ createPackFieldsList();
+ createUnusedFieldsList();
+ createPackPreview();
+
+ QGridLayout* mainLt = new QGridLayout;
+ mainLt->addLayout( m_packsListLt, 0, 0, 2, 1 );
+ mainLt->addLayout( m_fieldsListLt, 0, 1 );
+ mainLt->addLayout( m_unusedFieldsListLt, 1, 1 );
+ mainLt->addLayout( m_previewLt, 0, 2, 2, 1 );
+ setLayout( mainLt );
+
+ // Select first pack
+ m_packsListView->selectionModel()->setCurrentIndex( m_packFieldsListModel->index(0, 0),
+ QItemSelectionModel::Select );
+ }
+
+PacksPage::~PacksPage()
+ {
+ }
+
+void PacksPage::createPacksList()
+ {
+ m_packsListModel = new PacksListModel( m_parent );
+ m_packsListView = new PackFieldsView( this );
+ m_packsListView->setModel( m_packsListModel );
+ m_packsListView->setDropIndicatorShown(true);
+ m_packsListView->setMinimumWidth( 200 );
+
+ QPushButton* addPackBtn = new QPushButton( QPixmap(":/images/add.png"), tr("Add") );
+ QPushButton* removePackBtn = new QPushButton( QPixmap(":/images/delete.png"), tr("Remove") );
+ QHBoxLayout* packBtnLt = new QHBoxLayout;
+ packBtnLt->addWidget( addPackBtn );
+ packBtnLt->addWidget( removePackBtn );
+
+ connect( addPackBtn, SIGNAL(clicked()), this, SLOT(addPack()) );
+ connect( removePackBtn, SIGNAL(clicked()), this, SLOT(removePacks()) );
+
+ QToolButton* packMoveUpBtn = new QToolButton;
+ packMoveUpBtn->setObjectName("pack-up");
+ packMoveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ packMoveUpBtn->setToolTip(tr("Move pack up"));
+ QToolButton* packMoveDownBtn = new QToolButton;
+ packMoveDownBtn->setObjectName("pack-down");
+ packMoveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ packMoveDownBtn->setToolTip(tr("Move pack down"));
+ QVBoxLayout* packMoveBtnsLt = new QVBoxLayout;
+ packMoveBtnsLt->addWidget( packMoveUpBtn );
+ packMoveBtnsLt->addWidget( packMoveDownBtn );
+ packMoveBtnsLt->addStretch( 1 );
+
+ connect( packMoveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( packMoveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_packsListLt = new QGridLayout;
+ m_packsListLt->addWidget( new QLabel("<b>"+tr("Card packs")+"</b>"), 0, 0, 1, 2 );
+ m_packsListLt->addWidget( m_packsListView, 1, 0 );
+ m_packsListLt->addLayout( packMoveBtnsLt, 1, 1 );
+ m_packsListLt->addLayout( packBtnLt, 2, 0, 1, 2 );
+ }
+
+void PacksPage::createPackFieldsList()
+ {
+ m_packFieldsListModel = new PackFieldsListModel( m_parent );
+ m_fieldsListView = new PackFieldsView( this );
+ m_fieldsListView->setModel( m_packFieldsListModel );
+ m_fieldsListView->setDropIndicatorShown(true);
+
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ m_packFieldsListModel, SLOT(changeParentRow(const QModelIndex&)) );
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateUsesExactAnswer(const QModelIndex&)) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_packsListView, SLOT(updateCurrent()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_packsListView, SLOT(updateCurrent()) );
+
+ QToolButton* fieldMoveUpBtn = new QToolButton;
+ fieldMoveUpBtn->setObjectName("field-up");
+ fieldMoveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ fieldMoveUpBtn->setToolTip(tr("Move field up"));
+ QToolButton* fieldMoveDownBtn = new QToolButton;
+ fieldMoveDownBtn->setObjectName("field-down");
+ fieldMoveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ fieldMoveDownBtn->setToolTip(tr("Move field down"));
+ QVBoxLayout* fieldBtnsLt = new QVBoxLayout;
+ fieldBtnsLt->addWidget( fieldMoveUpBtn );
+ fieldBtnsLt->addWidget( fieldMoveDownBtn );
+ fieldBtnsLt->addStretch( 1 );
+
+ connect( fieldMoveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( fieldMoveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_fieldsListLt = new QGridLayout;
+ m_fieldsListLt->addWidget( new QLabel("<b>"+tr("Pack fields")+"</b>"), 0, 0, 1, 2 );
+ m_fieldsListLt->addWidget( m_fieldsListView, 1, 0 );
+ m_fieldsListLt->addLayout( fieldBtnsLt, 1, 1 );
+
+ }
+
+void PacksPage::createUnusedFieldsList()
+ {
+ m_unusedFieldsListModel = new UnusedFieldsListModel( m_parent );
+ m_unusedFieldsListView = new PackFieldsView( this );
+ m_unusedFieldsListView->setModel( m_unusedFieldsListModel );
+
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ m_unusedFieldsListModel, SLOT(changeParentRow(const QModelIndex&)) );
+
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+
+ QToolButton* fieldRemoveBtn = new QToolButton;
+ fieldRemoveBtn->setIcon(QIcon(":/images/down.png"));
+ fieldRemoveBtn->setToolTip(tr("Remove field from pack"));
+ QToolButton* fieldAddBtn = new QToolButton;
+ fieldAddBtn->setIcon(QIcon(":/images/up.png"));
+ fieldAddBtn->setToolTip(tr("Add field to pack"));
+ QHBoxLayout* usedFieldsBtnsLt = new QHBoxLayout;
+ usedFieldsBtnsLt->addWidget( fieldRemoveBtn );
+ usedFieldsBtnsLt->addWidget( fieldAddBtn );
+
+ connect( fieldRemoveBtn, SIGNAL(clicked()), this, SLOT(removeFields()) );
+ connect( fieldAddBtn, SIGNAL(clicked()), this, SLOT(addFields()) );
+
+ usesExactAnswerBox = new QCheckBox(tr("Uses exact answer"));
+ connect(usesExactAnswerBox,
+ SIGNAL(stateChanged(int)), SLOT(updatePackUsesExactAnswer(int)));
+
+ m_unusedFieldsListLt = new QGridLayout;
+ m_unusedFieldsListLt->addLayout( usedFieldsBtnsLt, 0, 0 );
+ m_unusedFieldsListLt->addWidget( new QLabel("<b>"+tr("Unused fields")+"</b>"), 0, 1 );
+ m_unusedFieldsListLt->addWidget( m_unusedFieldsListView, 1, 0, 1, 2 );
+ m_unusedFieldsListLt->addWidget(usesExactAnswerBox, 2, 0, 1, 2);
+ }
+
+void PacksPage::createPackPreview()
+ {
+ m_qstPreview = new CardSideView( CardSideView::QstMode );
+ m_qstPreview->setMinimumSize( 200, 70 );
+ m_ansPreview = new CardSideView( CardSideView::AnsMode );
+ m_ansPreview->setMinimumSize( 200, 70 );
+ m_previewLt = new QVBoxLayout;
+ m_previewLt->addWidget( new QLabel( "<b>"+tr("Preview")+"</b>") );
+ m_previewLt->addWidget( m_qstPreview, 1 );
+ m_previewLt->addWidget( m_ansPreview, 1 );
+
+ // From: the packs list
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updatePreviewForPack()) );
+
+ // From: the pack fields list
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+
+ // From: the dictionary field list at the "Fields" page
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updatePreviewForPack()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+ }
+
+void PacksPage::updatePreviewForPack()
+ {
+ int selectedPack = m_packsListView->selectionModel()->currentIndex().row();
+ if( selectedPack > -1 ) // If any selection
+ m_curPack = selectedPack;
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ if( !pack )
+ return;
+
+ const Field* qstField = pack->getQuestionField();
+ if( !qstField )
+ return;
+ QStringList ansList;
+ foreach( const Field* field, pack->getAnswerFields() )
+ ansList << field->name();
+ m_qstPreview->setPack( pack );
+ m_qstPreview->setQuestion( qstField->name() );
+ m_ansPreview->setPack( pack );
+ m_ansPreview->setQstAnsr( qstField->name(), ansList );
+ }
+
+void PacksPage::moveItemsUpDown()
+ {
+ PackFieldsView* view;
+ if( sender()->objectName().contains("pack") )
+ view = m_packsListView;
+ else if( sender()->objectName().contains("field") )
+ view = m_fieldsListView;
+ else
+ return;
+ DraggableListModel* model = view->model();
+
+ QModelIndexList selectedIndexes = view->selectionModel()->selectedIndexes();
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pSrcIndexes << QPersistentModelIndex( idx );
+ int rowsNum = pSrcIndexes.size();
+ if( rowsNum == 0 )
+ return;
+ qSort( pSrcIndexes );
+
+ int beginRow;
+ if( sender()->objectName().contains("up") )
+ beginRow = pSrcIndexes[0].row()-1;
+ else if( sender()->objectName().contains("down") )
+ beginRow = pSrcIndexes[rowsNum-1].row()+2;
+ else
+ return;
+ if( rowsNum == 1 && (beginRow < 0 || beginRow > model->rowCount()) )
+ return;
+ if( beginRow < 0 )
+ beginRow = 0;
+ if( beginRow > model->rowCount() )
+ beginRow = model->rowCount();
+
+ QMimeData* mime = model->mimeData( selectedIndexes );
+ model->dropMimeData( mime, Qt::MoveAction, beginRow, 0, QModelIndex() );
+ foreach (QModelIndex index, pSrcIndexes)
+ model->removeRow( index.row() );
+ }
+
+void PacksPage::removeFields()
+ {
+ if( m_packFieldsListModel->rowCount() <= 1 )
+ return;
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedIndexes();
+ if( selectedIndexes.isEmpty() )
+ return;
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pSrcIndexes << QPersistentModelIndex( idx );
+ foreach (QModelIndex index, pSrcIndexes)
+ m_fieldsListView->model()->removeRow( index.row() );
+ }
+
+void PacksPage::addFields()
+ {
+ QModelIndexList selectedIndexes = m_unusedFieldsListView->selectionModel()->selectedIndexes();
+ if( selectedIndexes.isEmpty() )
+ return;
+ int dstRow = m_fieldsListView->currentIndex().row()+1;
+ QMimeData* mime = m_unusedFieldsListModel->mimeData( selectedIndexes );
+ m_packFieldsListModel->dropMimeData( mime, Qt::MoveAction, dstRow, 0, QModelIndex() );
+ }
+
+
+void PacksPage::addPack()
+ {
+ QModelIndexList selectedIndexes = m_packsListView->selectionModel()->selectedRows();
+ int row;
+ if( selectedIndexes.size() > 0 )
+ {
+ qSort(selectedIndexes);
+ row = selectedIndexes[selectedIndexes.size()-1].row() + 1;
+ }
+ else
+ row = 0;
+ m_packsListModel->insertRow( row, QModelIndex() );
+ }
+
+void PacksPage::removePacks()
+ {
+ QModelIndexList selectedIndexes = m_packsListView->selectionModel()->selectedRows();
+ QList<QPersistentModelIndex> pIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pIndexes << QPersistentModelIndex( idx );
+ if( pIndexes.size() == 0 )
+ return;
+ foreach( QModelIndex idx, pIndexes )
+ m_packsListModel->removePack( idx.row() );
+ }
+
+void PacksPage::renamePack()
+ {
+ QModelIndex idx = m_packsListView->currentIndex();
+ if( !idx.isValid() )
+ return;
+ m_packsListView->edit( idx );
+ }
+
+void PacksPage::updateUsesExactAnswer(const QModelIndex& index)
+{
+ int selectedPack = index.row();
+ if( selectedPack > -1 ) // If any selection
+ m_curPack = selectedPack;
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ if( !pack )
+ return;
+ usesExactAnswerBox->setChecked(pack->getUsesExactAnswer());
+}
+
+void PacksPage::updatePackUsesExactAnswer(int state)
+{
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ pack->setUsesExactAnswer(state == Qt::Checked);
+}
diff --git a/src/dic-options/PacksPage.h b/src/dic-options/PacksPage.h
new file mode 100644
index 0000000..e191367
--- /dev/null
+++ b/src/dic-options/PacksPage.h
@@ -0,0 +1,70 @@
+#ifndef PACKSPAGE_H
+#define PACKSPAGE_H
+
+#include <QWidget>
+#include <QAbstractListModel>
+#include <QModelIndex>
+#include <QListView>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QGridLayout>
+
+#include "DictionaryOptionsDialog.h"
+#include "PackFieldsView.h"
+
+class CardSideView;
+class Dictionary;
+class PacksListModel;
+class UnusedFieldsListModel;
+
+class PacksPage : public QWidget
+{
+ Q_OBJECT
+public:
+ PacksPage( DictionaryOptionsDialog* aParent );
+ ~PacksPage();
+
+public slots:
+ void moveItemsUpDown();
+ void removeFields();
+ void addFields();
+ void addPack();
+ void removePacks();
+ void renamePack();
+
+private:
+ void createPacksList();
+ void createPackFieldsList();
+ void createUnusedFieldsList();
+ void createPackPreview();
+
+private slots:
+ void updatePreviewForPack();
+ void updateUsesExactAnswer(const QModelIndex& index);
+ void updatePackUsesExactAnswer(int state);
+
+private:
+ DictionaryOptionsDialog* m_parent;
+
+ // Models
+ PacksListModel* m_packsListModel;
+ QAbstractListModel* m_packFieldsListModel;
+ UnusedFieldsListModel* m_unusedFieldsListModel;
+ int m_curPack;
+
+ // List views
+ PackFieldsView* m_packsListView;
+ PackFieldsView* m_fieldsListView;
+ PackFieldsView* m_unusedFieldsListView;
+ QCheckBox* usesExactAnswerBox;
+ CardSideView* m_qstPreview;
+ CardSideView* m_ansPreview;
+
+ // Layouts
+ QGridLayout* m_packsListLt;
+ QGridLayout* m_fieldsListLt;
+ QGridLayout* m_unusedFieldsListLt;
+ QVBoxLayout* m_previewLt;
+};
+
+#endif
diff --git a/src/dic-options/UnusedFieldsListModel.cpp b/src/dic-options/UnusedFieldsListModel.cpp
new file mode 100644
index 0000000..3311285
--- /dev/null
+++ b/src/dic-options/UnusedFieldsListModel.cpp
@@ -0,0 +1,111 @@
+#include "UnusedFieldsListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Dictionary.h"
+
+#include <QMimeData>
+
+UnusedFieldsListModel::UnusedFieldsListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent ), m_parentRow( 0 )
+ {
+ updateUnusedFields();
+ }
+
+
+int UnusedFieldsListModel::rowCount(const QModelIndex& /*parent*/) const
+ {
+ return m_unusedFields.size();
+ }
+
+QVariant UnusedFieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return m_unusedFields.value(index.row())->name();
+ default:
+ return QVariant();
+ }
+ }
+
+void UnusedFieldsListModel::updateUnusedFields()
+ {
+ beginResetModel();
+ m_unusedFields.clear();
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return;
+ QList<const Field*> packFields = pack->getFields();
+ for( int i=0; i < m_parent->m_dict.fieldsNum(); i++ )
+ {
+ const Field* dictField = m_parent->m_dict.field( i );
+ if( !packFields.contains( dictField ) )
+ m_unusedFields << dictField;
+ }
+ endResetModel();
+ }
+
+void UnusedFieldsListModel::changeParentRow( const QModelIndex& aIndex )
+ {
+ m_parentRow = aIndex.row();
+ updateUnusedFields();
+ emit layoutChanged();
+ }
+
+bool UnusedFieldsListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ const Field* field = m_parent->m_dict.field( aValue.toString() );
+ if( !field )
+ return false;
+ m_unusedFields[index.row()] = field;
+ emit dataChanged(index, index);
+ return true;
+ }
+
+bool UnusedFieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_unusedFields.insert( position, m_parent->m_emptyField );
+ endInsertRows();
+ updateUnusedFields();
+ return true;
+ }
+
+bool UnusedFieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_unusedFields.removeAt( position );
+ endRemoveRows();
+ updateUnusedFields();
+ return true;
+ }
+
+const void* UnusedFieldsListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ const Field* field = m_unusedFields.value( aIndex.row() );
+ return static_cast<const void*>( field );
+ }
+
+void UnusedFieldsListModel::insertPointer( int aPos, void* aData )
+ {
+ m_unusedFields.insert( aPos, static_cast<Field*>( aData ) );
+ }
+
+bool UnusedFieldsListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int /*row*/, int /*column*/, const QModelIndex &/*parent*/)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+ return true;
+ }
diff --git a/src/dic-options/UnusedFieldsListModel.h b/src/dic-options/UnusedFieldsListModel.h
new file mode 100644
index 0000000..c3a95f6
--- /dev/null
+++ b/src/dic-options/UnusedFieldsListModel.h
@@ -0,0 +1,35 @@
+#ifndef UNUSEDFIELDSLISTMODEL_H
+#define UNUSEDFIELDSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+
+class UnusedFieldsListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ UnusedFieldsListModel( DictionaryOptionsDialog* aParent );
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const;
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent);
+
+public slots:
+ void changeParentRow( const QModelIndex& aIndex );
+ void updateUnusedFields();
+
+private:
+ int m_parentRow;
+ QList<const Field*> m_unusedFields; ///< Always updated list
+};
+
+#endif
diff --git a/src/dictionary/Card.cpp b/src/dictionary/Card.cpp
new file mode 100644
index 0000000..6464941
--- /dev/null
+++ b/src/dictionary/Card.cpp
@@ -0,0 +1,125 @@
+#include <QtDebug>
+
+#include "Card.h"
+#include "ICardPack.h"
+#include "DicRecord.h"
+#include "Field.h"
+
+bool Card::operator==( const Card& another ) const
+ {
+ return question == another.getQuestion();
+ }
+
+/** Card name is its question.
+ Image tags are replaced with image names.
+ */
+QString Card::getName() const
+ {
+ QString nameStr = question;
+ QRegExp imageNameRx("<img\\s*src=\"(.+[/\\\\])?(.+)\".*>");
+ nameStr.replace( imageNameRx, "\\2");
+ return nameStr;
+ }
+
+QStringList Card::getAnswers()
+ {
+ generateAnswers();
+ return answers;
+ }
+
+QList<const DicRecord*> Card::getSourceRecords()
+ {
+ if( sourceRecords.isEmpty() )
+ generateAnswers();
+ return sourceRecords;
+ }
+
+QMultiHash<QString, QString> Card::getAnswerElements()
+{
+ QMultiHash<QString, QString> answerElements; // Key: field name
+ QString qstFieldName = cardPack->getQuestionFieldName();
+ foreach(DicRecord* record, cardPack->getRecords())
+ if(record->getFieldElements(qstFieldName).contains(question) &&
+ record->isValid(qstFieldName))
+ {
+ sourceRecords << record;
+ connect(record, SIGNAL(valueChanged(QString)), SLOT(dropAnswers()));
+ foreach(QString ansFieldName, cardPack->getAnswerFieldNames())
+ {
+ if(record->getFieldValue(ansFieldName).isEmpty())
+ continue;
+ foreach(QString ansElement, record->getFieldElements(ansFieldName))
+ if(!answerElements.contains(ansFieldName, ansElement))
+ answerElements.insert(ansFieldName, ansElement);
+ }
+ }
+ return answerElements;
+}
+
+void Card::generateAnswersFromElements(
+ const QMultiHash<QString, QString>& answerElements)
+{
+ foreach(const Field* ansField, cardPack->getAnswerFields())
+ {
+ if(!ansField)
+ {
+ answers << "";
+ continue;
+ }
+ QStringList elements = getAnswerElementsForField(answerElements, ansField->name());
+ if(elements.isEmpty())
+ {
+ answers << "";
+ continue;
+ }
+ answers << elements.join(ICardPack::HomonymDelimiter);
+ }
+}
+
+QStringList Card::getAnswerElementsForField(const QMultiHash<QString, QString>& answerElements,
+ const QString& fieldName) const
+{
+ QStringList elements = answerElements.values(fieldName); // reversed order
+ QStringList elementsInOrder;
+ elementsInOrder.reserve(elements.size());
+ std::reverse_copy( elements.begin(), elements.end(),
+ std::back_inserter(elementsInOrder) );
+ return elementsInOrder;
+}
+
+// Fills also list of source entries
+void Card::generateAnswers()
+ {
+ if(!cardPack)
+ return;
+ clearAnswers();
+ generateAnswersFromElements(getAnswerElements());
+ }
+
+void Card::clearAnswers()
+{
+ answers.clear();
+ foreach(const DicRecord* record, sourceRecords)
+ disconnect( record, 0, this, SLOT(dropAnswers()) );
+ sourceRecords.clear();
+}
+
+void Card::dropAnswers()
+ {
+ clearAnswers();
+ emit answersChanged();
+ }
+
+StudyRecord Card::getStudyRecord() const
+ {
+ return cardPack->getStudyRecord(getQuestion());
+ }
+
+///< Reviewed at least twice
+bool Card::isScheduledAndReviewed() const
+ {
+ if(!cardPack)
+ return false;
+ int reviews = cardPack->getStudyRecords(getQuestion()).size();
+ return reviews >= 2;
+ }
diff --git a/src/dictionary/Card.h b/src/dictionary/Card.h
new file mode 100644
index 0000000..bd822e7
--- /dev/null
+++ b/src/dictionary/Card.h
@@ -0,0 +1,50 @@
+#ifndef CARD_H
+#define CARD_H
+
+#include <QtCore>
+
+#include "../study/StudyRecord.h"
+
+class Dictionary;
+class DicRecord;
+class ICardPack;
+
+class Card: public QObject
+{
+Q_OBJECT
+
+public:
+ Card(const ICardPack* cardPack, QString qst = ""):
+ cardPack(cardPack), question(qst) {}
+
+ bool operator==( const Card& another ) const;
+ QString getName() const;
+ const ICardPack* getCardPack() const { return cardPack; }
+ QString getQuestion() const { return question; }
+ QStringList getAnswers();
+ QList<const DicRecord*> getSourceRecords();
+ StudyRecord getStudyRecord() const;
+ bool isScheduledAndReviewed() const;
+
+private:
+ void generateAnswers();
+ void clearAnswers();
+ QMultiHash<QString, QString> getAnswerElements();
+ void generateAnswersFromElements(const QMultiHash<QString, QString>& answerElements);
+ QStringList getAnswerElementsForField(const QMultiHash<QString, QString>& answerElements,
+ const QString& fieldName) const;
+
+private slots:
+ void dropAnswers();
+
+signals:
+ void answersChanged() const;
+
+private:
+ const ICardPack* cardPack;
+ QString question;
+ QStringList answers;
+ QList<const DicRecord*> sourceRecords;
+};
+
+#endif
diff --git a/src/dictionary/CardPack.cpp b/src/dictionary/CardPack.cpp
new file mode 100644
index 0000000..75f5924
--- /dev/null
+++ b/src/dictionary/CardPack.cpp
@@ -0,0 +1,432 @@
+#include "CardPack.h"
+#include "Field.h"
+#include "DicRecord.h"
+#include "Card.h"
+#include "../study/StudyRecord.h"
+#include "../study/StudySettings.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+const QString CardPack::SynonymDelimiter = ";";
+const QString CardPack::HomonymDelimiter = "; ";
+
+CardPack::CardPack(IDictionary* dict):
+ m_dictionary(dict), isReadingStudyFile(false), usesExactAnswer(false)
+{
+ enableDictRecordUpdates();
+}
+
+/// Copies fields and study data from another pack.
+CardPack::CardPack(IDictionary* dict, const CardPack* otherPack ):
+ m_dictionary(dict)
+{
+ enableDictRecordUpdates();
+
+ // Copy fields by name
+ foreach( const Field* otherField, otherPack->fields )
+ {
+ Q_ASSERT( otherField );
+ if( !otherField )
+ return;
+ const Field* field = m_dictionary->field( otherField->name() );
+ fields << field;
+ }
+
+ // Copy study data
+ m_curCardName = otherPack->m_curCardName;
+ studyRecords = otherPack->studyRecords;
+ usesExactAnswer = otherPack->usesExactAnswer;
+}
+
+CardPack::~CardPack()
+ {
+ foreach( Card* card, cards )
+ delete card;
+ }
+
+QList<DicRecord*> CardPack::getRecords() const
+ {
+ if(!m_dictionary)
+ return QList<DicRecord*>();
+ return m_dictionary->getRecords();
+ }
+
+const TreeItem* CardPack::parent() const
+ {
+ return dynamic_cast<const TreeItem*>(m_dictionary);
+ }
+
+QVariant CardPack::data( int aColumn ) const
+ {
+ switch( aColumn )
+ {
+ case 0:
+ return id();
+ case 1:
+ return getActiveCards().size();
+ case 2:
+ return getNewCards().size();
+ default:
+ return QVariant();
+ }
+ }
+
+int CardPack::row() const
+ {
+ return m_dictionary->indexOfCardPack( const_cast<CardPack*>(this) );
+ }
+
+int CardPack::topParentRow() const
+ {
+ return dynamic_cast<TreeItem*>(m_dictionary)->row();
+ }
+
+/** Contains cards with at least 1 study record
+ */
+bool CardPack::containsReviewedCards() const
+ {
+ foreach( QString cardId, studyRecords.uniqueKeys() )
+ if( studyRecords.values( cardId ).size() >= 1 )
+ return true;
+ return false;
+ }
+
+StudyRecord CardPack::getStudyRecord(QString cardId) const
+{
+ QList<StudyRecord> recordList = studyRecords.values(cardId);
+ return recordList.isEmpty() ? StudyRecord() : recordList.first();
+}
+
+QString CardPack::findLastReviewedCard() const
+ {
+ QDateTime lastReview;
+ QString lastCardName;
+ foreach( QString cardName, cardQuestions )
+ {
+ QDateTime reviewed = getStudyRecord( cardName ).date;
+ if( reviewed > lastReview )
+ {
+ lastReview = reviewed;
+ lastCardName = cardName;
+ }
+ }
+ return lastCardName;
+ }
+
+QString CardPack::id() const
+{
+ if( !m_name.isEmpty() )
+ return m_name;
+ if( fields.empty() || !getQuestionField() )
+ return tr("(empty pack)");
+ return getQuestionFieldName() + " - " + getAnswerFieldNames().join(", ");
+}
+
+const Field* CardPack::getQuestionField() const
+{
+ if(fields.empty())
+ return NULL;
+ return fields.first();
+}
+
+QList<const Field*> CardPack::getAnswerFields() const
+{
+ return fields.mid(1);
+}
+
+// If the card is not yet created, it creates the card.
+Card* CardPack::getCard(const QString& cardName)
+{
+ if(cardName.isEmpty())
+ return NULL;
+ if(!cardQuestions.contains(cardName))
+ return NULL;
+ if( cards.contains( cardName ) )
+ return cards.value( cardName );
+ else
+ {
+ Card* card = new Card( this, cardName );
+ cards.insert( cardName, card );
+ return card;
+ }
+}
+
+void CardPack::setField(int aPos, const Field *aField)
+{
+ if( aPos >= fields.size() )
+ return;
+ fields[aPos] = aField;
+}
+
+void CardPack::setQstField(const Field *aField)
+{
+ if( !fields.empty() )
+ fields[0] = aField;
+ else
+ fields << aField;
+}
+
+void CardPack::setAnsFields(QList<const Field*> aFields)
+ {
+ const Field* questionField = NULL;
+ if(!fields.isEmpty())
+ questionField = fields.first();
+ fields.clear();
+ fields << questionField << aFields;
+ }
+
+void CardPack::destroyCards()
+{
+ cardQuestions.clear();
+ foreach(Card* card, cards)
+ delete card;
+ cards.clear();
+}
+
+void CardPack::addQuestionElementsForRecord(const DicRecord* record)
+{
+ foreach(QString qstElement, record->getFieldElements(getQuestionFieldName()))
+ {
+ if(!cardQuestions.contains(qstElement))
+ cardQuestions << qstElement;
+ }
+}
+
+void CardPack::removeAbsentCards(QStringList& cardQuestions)
+{
+ QMutableListIterator<QString> cardIt(cardQuestions);
+ while(cardIt.hasNext())
+ if(!cardQuestions.contains(cardIt.next()))
+ cardIt.remove();
+}
+
+void CardPack::generateQuestions()
+ {
+ destroyCards();
+
+ if(fields.size() < MinRequiredFieldsNum)
+ return;
+ if(!getQuestionField())
+ return;
+
+ foreach(DicRecord* record, getRecords())
+ if(record->isValid(getQuestionFieldName()))
+ addQuestionElementsForRecord(record);
+
+ emit cardsGenerated();
+ }
+
+QStringList CardPack::getNewCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ if(!studyRecords.contains(cardName))
+ list << cardName;
+ return list;
+}
+
+QStringList CardPack::getActiveCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ if(getStudyRecord(cardName).timeTriggered())
+ list << cardName;
+ return list;
+}
+
+int CardPack::getActiveRepeatingCardsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.timeTriggered() && study.level == StudyRecord::Repeating)
+ count++;
+ }
+ return count;
+}
+
+int CardPack::countScheduledForTodayCards() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isActivatedToday())
+ {
+ if(study.isLearning())
+ count += study.getScheduledTodayReviews();
+ else
+ count++;
+ }
+ }
+ return count;
+}
+
+// With small intervals < 1 day
+QStringList CardPack::getPriorityActiveCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.timeTriggered() && study.interval < 1)
+ list << cardName;
+ }
+ return list;
+}
+
+QStringList CardPack::getLearningCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning())
+ list << cardName;
+ }
+ return list;
+}
+
+int CardPack::getLearningReviewsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning())
+ count += study.getScheduledTodayReviews();
+ }
+ return count;
+}
+
+int CardPack::getTimeToNextLearning() const
+{
+ int minTime = 10000;
+ foreach(QString cardName, getLearningCards())
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ int left = study.getSecsToNextRepetition();
+ if(left < minTime)
+ minTime = left;
+ }
+ if(minTime == 10000)
+ minTime = 0;
+ return minTime;
+}
+
+int CardPack::getInactiveLearningReviewsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning() && !study.timeTriggered())
+ count += study.getScheduledTodayReviews();
+ }
+ return count;
+}
+
+QStringList CardPack::getSmallestIntervalCards(const QStringList& priorityCards)
+{
+ QStringList smallestIntervals;
+ double smallest = 1;
+ foreach(QString name, priorityCards)
+ {
+ StudyRecord study = getStudyRecord(name);
+ if(study.interval < smallest)
+ {
+ smallest = study.interval;
+ smallestIntervals.clear();
+ smallestIntervals << name;
+ }
+ else if(study.interval == smallest)
+ smallestIntervals << name;
+ }
+ return smallestIntervals;
+}
+
+int CardPack::getTodayReviewedCardsNum() const
+{
+ const int MaxStudyDepth = 3;
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ const QList<StudyRecord> studyList = studyRecords.values(cardName);
+ for(int j = 0; j < studyList.size(); j++)
+ {
+ if(j >= MaxStudyDepth)
+ break;
+ if(studyList[j].isReviewedToday())
+ count++;
+ }
+ }
+ return count;
+}
+
+int CardPack::getTodayNewCardsNum() const
+{
+ int count = 0;
+ foreach( QString cardName, cardQuestions )
+ {
+ const QList<StudyRecord> studyList = studyRecords.values(cardName);
+ if(studyList.isEmpty())
+ continue;
+ StudyRecord firstStudy = studyList.last();
+ if(firstStudy.isReviewedToday())
+ count++;
+ }
+ return count;
+}
+
+void CardPack::addStudyRecord( const QString aCardId, const StudyRecord& aStudyRecord )
+ {
+ if(!cardQuestions.contains(aCardId))
+ return;
+ studyRecords.insert(aCardId, aStudyRecord);
+ if(!isReadingStudyFile)
+ emit studyRecordAdded();
+ }
+
+void CardPack::setCurCard( const QString aCardId )
+ {
+ if( aCardId == m_curCardName )
+ return;
+ m_curCardName = aCardId;
+ emit studyRecordAdded(); // study is modified by the cur card
+ }
+
+QList<QDateTime> CardPack::getScheduledDates() const
+{
+ const int secsInDay = 24 * 60 * 60;
+ QList<QDateTime> scheduled;
+ foreach(QString cardName, studyRecords.uniqueKeys())
+ {
+ StudyRecord record = getStudyRecord(cardName);
+ scheduled << record.date.addSecs((int)(record.interval * secsInDay));
+ }
+ return scheduled;
+}
+
+void CardPack::processEntryChangedEvent( int aEntryIx, int aFieldIx )
+ {
+ Q_UNUSED( aEntryIx )
+ if(aFieldIx != IDictionary::AllFields)
+ generateQuestions();
+ }
+
+void CardPack::disableDictRecordUpdates()
+ {
+ disconnect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entryChanged(int,int)), this, SLOT(processEntryChangedEvent(int,int)) );
+ disconnect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entriesRemoved(int,int)), this, SLOT(processEntryChangedEvent(int)) );
+ }
+
+void CardPack::enableDictRecordUpdates()
+ {
+ connect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entryChanged(int,int)), SLOT(processEntryChangedEvent(int,int)) );
+ connect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entriesRemoved(int,int)), SLOT(processEntryChangedEvent(int)) );
+ // Inserting empty records doesn't regenerate cards
+ }
diff --git a/src/dictionary/CardPack.h b/src/dictionary/CardPack.h
new file mode 100644
index 0000000..fc7d335
--- /dev/null
+++ b/src/dictionary/CardPack.h
@@ -0,0 +1,144 @@
+#ifndef CARDPACK_H
+#define CARDPACK_H
+
+#include <QtCore>
+#include <QtDebug>
+
+#include "ICardPack.h"
+#include "../study/StudyRecord.h"
+#include "IDictionary.h"
+#include "Field.h"
+#include "TreeItem.h"
+
+class DicRecord;
+class Card;
+
+/**
+ @brief Pack of cards
+
+ A card pack consists of cards of certain card pattern.
+ A card pattern is a combination of fields: question field and answer fields.
+*/
+
+class CardPack: public TreeItem, public ICardPack
+{
+ Q_OBJECT
+
+public:
+ static const QString SynonymDelimiter;
+ static const QString HomonymDelimiter;
+
+public:
+ CardPack(IDictionary* dict);
+ CardPack(IDictionary* dict, const CardPack* otherPack);
+ ~CardPack();
+
+ // Getters
+
+ QString id() const;
+ QString name() const { return m_name; } // See also id()
+ QString curCardName() const { return m_curCardName; }
+ IDictionary* dictionary() const { return m_dictionary; }
+ QList<DicRecord*> getRecords() const;
+
+ // Tree view
+ const TreeItem* parent() const;
+ const TreeItem* child( int /*aRow*/ ) const { return NULL; }
+ int childCount() const { return 0; }
+ int columnCount() const { return 3; }
+ QVariant data( int aColumn ) const;
+ int row() const;
+ int topParentRow() const;
+
+ // Fields
+ QList<const Field*> getFields() const { return fields; }
+ const Field* getQuestionField() const;
+ QList<const Field*> getAnswerFields() const;
+ bool getUsesExactAnswer() const { return usesExactAnswer; }
+
+ // Cards
+ QStringList getCardQuestions() const { return cardQuestions; }
+ bool containsQuestion(const QString& question) const
+ { return cardQuestions.contains(question); }
+ int cardsNum() const { return cardQuestions.size(); }
+ Card* getCard(const QString& cardName);
+
+ // Study records
+ QList<StudyRecord> getStudyRecords() const { return studyRecords.values(); }
+ QList<QDateTime> getScheduledDates() const;
+ QList<StudyRecord> getStudyRecords(QString cardId) const
+ { return studyRecords.values(cardId); }
+ int studyRecordsNum() const { return studyRecords.uniqueKeys().size(); }
+ bool containsReviewedCards() const;
+ StudyRecord getStudyRecord(QString cardId) const;
+ QString findLastReviewedCard() const;
+
+ // Study statistics
+ QStringList getNewCards() const;
+ QStringList getActiveCards() const;
+ int getActiveRepeatingCardsNum() const;
+ int countScheduledForTodayCards() const;
+ QStringList getPriorityActiveCards() const;
+ QStringList getLearningCards() const;
+ int getLearningReviewsNum() const;
+ int getTimeToNextLearning() const;
+ int getInactiveLearningReviewsNum() const;
+ QStringList getSmallestIntervalCards(const QStringList& priorityCards);
+ int getTodayReviewedCardsNum() const;
+ int getTodayNewCardsNum() const;
+
+public:
+ // Setters
+ void setName( QString aName ) { m_name = aName; }
+ void setCurCard( const QString aCardId );
+
+ // Fields
+ void setFields( QList<const Field*> aFields ) { fields = aFields; }
+ void setField( int aPos, const Field* aField );
+ void addField( const Field* aField ) { fields << aField; }
+ void removeField( int aPos ) { fields.removeAt(aPos); }
+ void removeField( const Field* aField ) { fields.removeAll( aField ); }
+ void insertField( int aPos, const Field* aField ) { fields.insert( aPos, aField ); }
+ void setQstField( const Field* aField );
+ void setAnsFields( QList<const Field*> aFields );
+ void setUsesExactAnswer(bool uses) { usesExactAnswer = uses; }
+
+ void setReadingStudyFile(bool reading) { isReadingStudyFile = reading; }
+ void addStudyRecord( const QString aCardId, const StudyRecord& aStudyRecord );
+
+public:
+ void generateQuestions();
+ void disableDictRecordUpdates();
+ void enableDictRecordUpdates();
+
+private:
+ void destroyCards();
+ void addQuestionElementsForRecord(const DicRecord* record);
+ void removeAbsentCards(QStringList& cardQuestions);
+ void removeDelayedCard(const QString& cardName);
+
+signals:
+ void studyRecordAdded();
+ void cardsGenerated();
+
+private slots:
+ void processEntryChangedEvent( int aEntryIx, int aFieldIx = IDictionary::AllFields );
+
+private:
+ static const int MinRequiredFieldsNum = 2;
+
+private:
+ QString m_name; // Not used
+ IDictionary* m_dictionary;
+
+ // The first field is the question, others are answers.
+ QList<const Field*> fields;
+
+ QStringList cardQuestions; ///< Original order of questions
+ QHash<QString, Card*> cards; ///< Card name -> card (own)
+ QString m_curCardName;
+ QMultiHash<QString, StudyRecord> studyRecords; // Card ID -> Study record
+ bool isReadingStudyFile;
+ bool usesExactAnswer;
+};
+#endif
diff --git a/src/dictionary/DicCsvReader.cpp b/src/dictionary/DicCsvReader.cpp
new file mode 100644
index 0000000..ec80225
--- /dev/null
+++ b/src/dictionary/DicCsvReader.cpp
@@ -0,0 +1,205 @@
+#include "DicCsvReader.h"
+#include "Dictionary.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+
+DicCsvReader::DicCsvReader( Dictionary* aDict ):
+ m_dict( aDict )
+ {
+ }
+
+DicCsvReader::DicCsvReader():
+ m_dict( NULL )
+ {
+ }
+
+QFile::FileError DicCsvReader::readDict( const QString aCsvFilePath, const CsvImportData& aImportData )
+ {
+ if( !m_dict )
+ return QFile::NoError;
+
+ initData( aImportData );
+
+ // Erase dictionary content
+ m_dict->clearFieldPackConfig();
+ if( m_dict->entriesNum() > 0 )
+ m_dict->removeRecords( 0, m_dict->entriesNum() );
+
+ QFile file( aCsvFilePath );
+ if( !file.open( QIODevice::ReadOnly | QFile::Text ) ) // \r\n --> \n
+ return file.error();
+ QTextStream inStream( &file );
+ inStream.setCodec( m_params.textCodec );
+
+ // Ignore first rows
+ int rowNum = 1;
+ while( !inStream.atEnd() && rowNum++ < m_params.fromLine )
+ inStream.readLine();
+
+ int fieldsNum = readLines( inStream );
+ file.close();
+
+ // Add entries to dictionary
+ foreach( DicRecord* entry, m_entries )
+ m_dict->addRecord(entry);
+
+ // Name nameless fields and create dictionary fields
+ for( int i = 0; i < fieldsNum; i++ )
+ {
+ QString name = m_fieldNames.value( i );
+ if( name.isEmpty() )
+ name = QString::number( i+1 );
+ m_dict->addField( name );
+ }
+
+ // Create packs
+ CardPack* pack;
+ QList<const Field*> ansFields;
+
+ pack = new CardPack( m_dict );
+ pack->setQstField( m_dict->field(0) );
+ ansFields << m_dict->field(1);
+ for( int i = 2; i < m_dict->fieldsNum(); i++ )
+ ansFields << m_dict->field(i);
+ pack->setAnsFields( ansFields );
+ m_dict->addCardPack( pack );
+
+ ansFields.clear();
+ pack = new CardPack( m_dict );
+ pack->setQstField( m_dict->field(1) );
+ ansFields << m_dict->field(0);
+ for( int i = 2; i < m_dict->fieldsNum(); i++ )
+ ansFields << m_dict->field(i);
+ pack->setAnsFields( ansFields );
+ m_dict->addCardPack( pack );
+
+ return QFile::NoError;
+ }
+
+// The read field names are returned with fieldNames().
+
+QList<DicRecord*> DicCsvReader::readEntries( QString aCsvEntries, const CsvImportData& aImportData )
+ {
+ initData( aImportData );
+ QTextStream inStream( &aCsvEntries );
+ readLines( inStream );
+ if( !m_fieldNames.empty() )
+ return m_entries;
+ else
+ return QList<DicRecord*>(); /* If no required header, they are not real entries.
+ * When pasting, this text cannot be parsed and must be ignored. */
+ }
+
+void DicCsvReader::initData( const CsvImportData& aImportData )
+ {
+ m_params = aImportData;
+ // Construct regexp for the separators
+ QString fieldSepRxStr;
+ switch( m_params.fieldSeparationMode )
+ {
+ case EFieldSeparatorAnyCharacter:
+ fieldSepRxStr = QString("[") + m_params.fieldSeparators + "]";
+ break;
+ case EFieldSeparatorAnyCombination:
+ fieldSepRxStr = QString("[") + m_params.fieldSeparators + "]+";
+ break;
+ case EFieldSeparatorExactString:
+ fieldSepRxStr = m_params.fieldSeparators;
+ break;
+ }
+ m_fieldSepRx = QRegExp( fieldSepRxStr );
+ QChar delim = m_params.textDelimiter;
+ m_chunkEndRx = QRegExp( QString("(") + fieldSepRxStr + "|" + delim + ")" ); // End of text chunk
+ m_singleDelimiterRx = QRegExp( QString("([^") + delim + "]|^)" + delim + "([^" + delim + "]|$)" ); // Single text delimiter
+ }
+
+int DicCsvReader::readLines( QTextStream& aInStream )
+ {
+ QString line;
+
+ // Read field names from the header
+ m_fieldNames.clear();
+ if( m_params.firstLineIsHeader && !aInStream.atEnd() )
+ {
+ line = aInStream.readLine();
+ if( line.startsWith( m_params.commentChar ) )
+ line.remove( m_params.commentChar );
+ m_fieldNames = readFields( line.trimmed() );
+ }
+
+ // Read lines and create entries
+ int fieldsNum = 0;
+ m_entries.clear();
+ while( !aInStream.atEnd() )
+ {
+ line = aInStream.readLine();
+ QStringList fields = readFields( line.trimmed() );
+ DicRecord* entry = new DicRecord();
+ for( int i = 0; i < fields.size(); i++ )
+ {
+ QString name = m_fieldNames.value( i, QString::number( i+1 ) );
+ QString field = fields.value( i );
+ entry->setField( name, field );
+ }
+ m_entries << entry; // Add even empty entries (without fields)
+ if( fields.size() > fieldsNum )
+ fieldsNum = fields.size();
+ }
+ return fieldsNum;
+ }
+
+QStringList DicCsvReader::readFields( const QString aCsvLine )
+ {
+ QChar comment = m_params.commentChar;
+ if( !comment.isNull() && aCsvLine.startsWith( comment ) )
+ return QStringList(); // FUTURE FEATURE: read and mark as disabled
+
+ QChar delim = m_params.textDelimiter;
+ QStringList fields;
+ int curPos = 0;
+ QString curText;
+ while( curPos < aCsvLine.length() )
+ {
+ QChar curChar = aCsvLine[curPos];
+ if( curChar == delim ) // Text delimiter - Process the text until the next delimiter
+ {
+ int quoteEnd = aCsvLine.indexOf( m_singleDelimiterRx, curPos + 1 );
+ if( quoteEnd == -1) // End of line
+ quoteEnd = aCsvLine.length();
+ curText += aCsvLine.mid( curPos+1, quoteEnd-curPos );
+ curPos = quoteEnd + 2; // move beyond the delimiter
+ }
+ else if( m_fieldSepRx.indexIn( aCsvLine, curPos ) == curPos ) // Field separator - End of field
+ {
+ int sepLength = m_fieldSepRx.matchedLength();
+ curPos += sepLength;
+ fields << unescapeString( curText.trimmed() );
+ curText.clear();
+ }
+ else // Chunk of normal text. Process until next field separator or text delimiter.
+ {
+ int chunkEnd = aCsvLine.indexOf( m_chunkEndRx, curPos );
+ if( chunkEnd == -1 ) // End of line
+ chunkEnd = aCsvLine.length();
+ curText += aCsvLine.mid( curPos, chunkEnd-curPos );
+ curPos = chunkEnd;
+ }
+ }
+ if( !curText.isEmpty() ) // last Field
+ fields << unescapeString( curText.trimmed() );
+
+ if( m_params.colsToImport > 0 ) // Take only needed imported fields
+ fields = fields.mid( 0, m_params.colsToImport );
+
+ return fields;
+}
+
+/**
+ * Replaces double delimiters with one delimiter
+ */
+QString DicCsvReader::unescapeString( QString aString )
+{
+ QString delim = m_params.textDelimiter;
+ aString = aString.replace( delim + delim, delim );
+ return aString;
+}
diff --git a/src/dictionary/DicCsvReader.h b/src/dictionary/DicCsvReader.h
new file mode 100644
index 0000000..9e990dd
--- /dev/null
+++ b/src/dictionary/DicCsvReader.h
@@ -0,0 +1,41 @@
+#ifndef DICCSVREADER_H
+#define DICCSVREADER_H
+
+#include <QFile>
+#include <QString>
+#include <QStringList>
+#include <QRegExp>
+#include <QTextStream>
+
+#include "../export-import/CsvData.h"
+
+class Dictionary;
+class DicRecord;
+
+class DicCsvReader
+{
+public:
+ DicCsvReader( Dictionary* aDict );
+ DicCsvReader(); // For reading entries
+
+ QFile::FileError readDict( const QString aCsvFilePath, const CsvImportData& aImportData );
+ QList<DicRecord*> readEntries( QString aCsvEntries, const CsvImportData& aImportData );
+ QStringList fieldNames() { return m_fieldNames; } // Last read field names
+ QString unescapeString( QString aString );
+
+private:
+ void initData( const CsvImportData& aImportData );
+ int readLines( QTextStream& aInStream ); // Returns max number of fields
+ QStringList readFields( const QString aCsvLine );
+
+private:
+ Dictionary* m_dict; // Not own
+ CsvImportData m_params;
+ QRegExp m_fieldSepRx;
+ QRegExp m_chunkEndRx;
+ QRegExp m_singleDelimiterRx;
+ QStringList m_fieldNames;
+ QList<DicRecord*> m_entries; // Created, but ownership is transfered to the client
+};
+
+#endif // DICCSVREADER_H
diff --git a/src/dictionary/DicCsvWriter.cpp b/src/dictionary/DicCsvWriter.cpp
new file mode 100644
index 0000000..f2a5e2e
--- /dev/null
+++ b/src/dictionary/DicCsvWriter.cpp
@@ -0,0 +1,110 @@
+#include "DicCsvWriter.h"
+#include "Dictionary.h"
+#include "DicRecord.h"
+#include "Field.h"
+
+#include <QTextStream>
+
+DicCsvWriter::DicCsvWriter( const Dictionary* aDict ):
+ m_dict( aDict )
+ {
+ }
+
+DicCsvWriter::DicCsvWriter( const QList<DicRecord*> aEntries ):
+ m_dict( NULL ), m_entries( aEntries )
+ {
+ }
+
+QString DicCsvWriter::toCsvString( const CsvExportData& aExportData )
+ {
+ m_params = aExportData;
+ if( m_params.quoteAllFields )
+ m_fieldSepRegExp = QRegExp("."); // Any character
+ else
+ m_fieldSepRegExp = QRegExp( m_params.fieldSeparators ); // Exact string of separators
+
+ QChar delimiter = m_params.textDelimiter;
+ QString outStr;
+ QTextStream outStream( &outStr );
+
+ // Generate list of selected fields
+ m_selectedFieldNames.clear();
+ if( !m_dict ) // from entries
+ {
+ foreach(DicRecord* entry, m_entries )
+ foreach( QString fieldName, entry->getFields().keys() )
+ if( !m_selectedFieldNames.contains( fieldName ) )
+ m_selectedFieldNames << fieldName;
+ }
+ else // from dictionary
+ {
+ if( !m_params.usedCols.isEmpty() )
+ foreach( int col, m_params.usedCols )
+ m_selectedFieldNames << m_dict->field(col)->name();
+ else // All fields
+ foreach( Field* field, m_dict->fields() )
+ m_selectedFieldNames << field->name();
+
+ }
+
+ // Write column names
+ if( m_params.writeColumnNames )
+ {
+ QStringList escapedNames;
+ foreach( QString name, m_selectedFieldNames )
+ {
+ if( !delimiter.isNull() && name.contains( m_fieldSepRegExp ) )
+ name = delimiter + name + delimiter;
+ escapedNames << name;
+ }
+ QString header = QString( m_params.commentChar ) + " ";
+ header += escapedNames.join( m_params.fieldSeparators );
+ outStream << header << endl;
+ }
+
+ // For dictionary, copy entries into the local list.
+ if( m_dict )
+ m_entries = m_dict->getRecords();
+
+ // Write entries
+ bool lastLineWasEmpty = false;
+ for( int i = 0; i < m_entries.size(); i++ )
+ {
+ QString curLine = dicEntryToString( m_entries.value( i ) );
+ if( !(lastLineWasEmpty && curLine.isEmpty()) ) // Don't print several empty lines in a row
+ outStream << curLine << endl;
+ lastLineWasEmpty = curLine.isEmpty();
+ }
+ return outStr;
+ }
+
+QString DicCsvWriter::dicEntryToString(const DicRecord* aEntry ) const
+ {
+ if( !aEntry )
+ return QString();
+
+ QStringList fieldValues; // Convert the fields map into string list. If needed, delimit text ("" or ').
+ QChar delimiter = m_params.textDelimiter;
+ foreach( QString key, m_selectedFieldNames )
+ {
+ QString value = aEntry->getFieldValue( key );
+ /* If the field has embedded field separator or text delimiter (quote),
+ * it must be quoted. */
+ if( !delimiter.isNull() && ( value.contains( m_fieldSepRegExp ) || value.contains(delimiter) ) )
+ {
+ if( value.contains(delimiter) ) // Embedded text delimiter (")
+ value.replace( delimiter, QString(delimiter) + delimiter ); // Escape it with double delimiter ("")
+ value = delimiter + value + delimiter;
+ }
+ fieldValues << value;
+ }
+ // Remove all last empty fields
+ while( !fieldValues.isEmpty() && fieldValues.last().isEmpty() )
+ fieldValues.removeLast();
+ QString res = fieldValues.join( m_params.fieldSeparators );
+
+ /* FUTURE FEATURE: if( iIsCommented )
+ csv.insert( 0, m_params.iCommentChar ); // Insert the comment character */
+ return res;
+ }
+
diff --git a/src/dictionary/DicCsvWriter.h b/src/dictionary/DicCsvWriter.h
new file mode 100644
index 0000000..3764309
--- /dev/null
+++ b/src/dictionary/DicCsvWriter.h
@@ -0,0 +1,31 @@
+#ifndef DICCSVWRITER_H
+#define DICCSVWRITER_H
+
+#include <QRegExp>
+#include <QStringList>
+
+#include "../export-import/CsvData.h"
+
+class Dictionary;
+class DicRecord;
+
+class DicCsvWriter
+{
+public:
+ DicCsvWriter( const Dictionary* aDict ); // For writing from a dictionary
+ DicCsvWriter( const QList<DicRecord*> aEntries ); // For writing from list of entries
+
+ QString toCsvString( const CsvExportData& aExportData ); // Both for writing from a dictionary and list of entries
+
+private:
+ QString dicEntryToString( const DicRecord* aEntry ) const;
+
+private:
+ const Dictionary* m_dict;
+ QList<DicRecord*> m_entries; // Used both for dictionary and entries
+ QStringList m_selectedFieldNames;
+ CsvExportData m_params;
+ QRegExp m_fieldSepRegExp;
+};
+
+#endif // DICCSVWRITER_H
diff --git a/src/dictionary/DicRecord.cpp b/src/dictionary/DicRecord.cpp
new file mode 100644
index 0000000..91cebca
--- /dev/null
+++ b/src/dictionary/DicRecord.cpp
@@ -0,0 +1,62 @@
+#include "DicRecord.h"
+#include "Field.h"
+#include "ICardPack.h"
+
+DicRecord::DicRecord()
+ {
+ }
+
+DicRecord::DicRecord( const DicRecord& aOther ):
+ QObject(0)
+ {
+ fields = aOther.fields;
+ m_id04 = aOther.m_id04;
+ }
+
+void DicRecord::setField( QString aField, QString aValue )
+ {
+ fields[aField] = aValue;
+ emit valueChanged( aField );
+ }
+
+void DicRecord::renameField( const QString aOldFieldName, const QString aNewFieldName )
+ {
+ if( !fields.keys().contains( aOldFieldName ) )
+ return;
+ QString value = fields.value( aOldFieldName );
+ fields.remove( aOldFieldName );
+ fields[ aNewFieldName ] = value;
+}
+
+bool DicRecord::isValid(const QString& qstFieldName) const
+{
+ return !fields.value(qstFieldName).isEmpty() &&
+ hasNonEmptyAnswerField( qstFieldName );
+}
+
+bool DicRecord::hasNonEmptyAnswerField(const QString& qstFieldName) const
+{
+ foreach(QString name, fields.keys())
+ if(name != qstFieldName && !fields.value(name).isEmpty())
+ return true;
+ return false;
+}
+
+QStringList DicRecord::getFieldElements(const QString& fieldName) const
+{
+ QString value = fields.value(fieldName);
+ QStringList elements = value.split(ICardPack::SynonymDelimiter, QString::SkipEmptyParts);
+ QStringList trimmedElements;
+ foreach(QString element, elements)
+ trimmedElements << element.trimmed();
+ return trimmedElements;
+}
+
+QString DicRecord::getPreviewQuestionForPack(ICardPack* pack) const
+{
+ if(!pack)
+ return QString();
+ QString fieldName = pack->getQuestionField()->name();
+ QString questionFieldValue = getFieldValue(fieldName);
+ return questionFieldValue.split(ICardPack::SynonymDelimiter).first().trimmed();
+}
diff --git a/src/dictionary/DicRecord.h b/src/dictionary/DicRecord.h
new file mode 100644
index 0000000..913da08
--- /dev/null
+++ b/src/dictionary/DicRecord.h
@@ -0,0 +1,40 @@
+#ifndef DICENTRY_H
+#define DICENTRY_H
+
+#include <QtCore>
+
+class Field;
+class ICardPack;
+
+class DicRecord: public QObject
+{
+Q_OBJECT
+
+public:
+ DicRecord();
+ DicRecord( const DicRecord& aOther );
+
+ const QHash<QString, QString> getFields() const {return fields;}
+ QString getFieldValue(const QString& name) const { return fields.value(name); }
+ QStringList getFieldElements(const QString& fieldName) const;
+ QString getPreviewQuestionForPack(ICardPack* pack) const;
+ QString id04() const { return m_id04; }
+ void setField( QString aField, QString aValue );
+ void setId04( const QString& aId ) { m_id04 = aId; }
+ void renameField( const QString aOldFieldName, const QString aNewFieldName );
+ bool isValid(const QString& qstFieldName) const;
+
+private:
+ bool hasNonEmptyAnswerField(const QString& qstFieldName) const;
+
+signals:
+ void valueChanged( QString aField );
+
+private:
+ QHash<QString, QString> fields; // Field name -> field value
+ QString m_id04; // For v. 0.4
+};
+
+Q_DECLARE_METATYPE( DicRecord )
+
+#endif
diff --git a/src/dictionary/Dictionary.cpp b/src/dictionary/Dictionary.cpp
new file mode 100644
index 0000000..0779651
--- /dev/null
+++ b/src/dictionary/Dictionary.cpp
@@ -0,0 +1,601 @@
+#include "Dictionary.h"
+#include "CardPack.h"
+#include "../version.h"
+#include "DicRecord.h"
+#include "DictionaryWriter.h"
+#include "DictionaryReader.h"
+#include "../study/StudyFileWriter.h"
+#include "../study/StudyFileReader.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../main-view/AppModel.h"
+
+#include <QTextStream>
+#include <QStringList>
+#include <QtAlgorithms>
+#include <QDateTime>
+#include <QSettings>
+#include <QDir>
+
+const QString Dictionary::DictFileExtension(".fmd");
+const QString Dictionary::StudyFileExtension(".fms");
+const char* Dictionary::NoName( QT_TRANSLATE_NOOP("Dictionary", "noname") );
+
+Dictionary::Dictionary(const QString& aFilePath, bool aNameIsTemp, const AppModel* aAppModel):
+ IDictionary(aFilePath), m_appModel(aAppModel),
+ obsoleteId(QUuid()),
+ m_contentModified(true), m_studyModified(false), m_nameIsTemp(aNameIsTemp)
+ {
+ }
+
+Dictionary::~Dictionary()
+ {
+ foreach( Field* field, m_fields )
+ delete field;
+ foreach( CardPack* pack, m_cardPacks )
+ delete pack;
+ }
+
+const TreeItem* Dictionary::child( int aRow ) const
+ {
+ return cardPack( aRow );
+ }
+
+QVariant Dictionary::data( int aColumn ) const
+ {
+ if( aColumn == 0 )
+ return shortName( false );
+ else
+ return QVariant();
+ }
+
+int Dictionary::row() const
+ {
+ if( m_appModel )
+ return m_appModel->indexOfDictionary( const_cast<Dictionary*>(this) );
+ else
+ return 0;
+ }
+
+void Dictionary::clearFieldPackConfig()
+ {
+ while( !m_fields.isEmpty() )
+ delete m_fields.takeLast();
+ while( !m_cardPacks.isEmpty() )
+ delete m_cardPacks.takeLast();
+ }
+
+/** Copies from another dictionary:
+ * - fields, packs, study data.
+ * The old configuration is deleted.
+ * The dic records are not changed.
+ */
+void Dictionary::setDictConfig( const Dictionary* aOtherDic )
+ {
+ clearFieldPackConfig();
+
+ // Replace fields
+ foreach( Field* f, aOtherDic->fields() )
+ {
+ m_fields << new Field( f->name(), f->style() );
+ // Fix the renamed fields in the entries
+ // TODO: Refactor to remove the old name
+ foreach( DicRecord* entry, records )
+ entry->renameField( f->oldName(), f->name() );
+ }
+
+ // Replace card packs
+ foreach( CardPack* otherPack, aOtherDic->cardPacks() )
+ {
+ /* The packs are copied together with study data, because
+ * afterwards it's impossible to find correct modified pack
+ */
+ CardPack* newPack = new CardPack( this, otherPack );
+ addCardPack( newPack );
+ }
+
+ setContentModified();
+ generateCards();
+ }
+
+void Dictionary::setDefaultFields()
+{
+ m_fields << new Field( tr("Question"), FieldStyleFactory::DefaultStyle );
+ m_fields << new Field( tr("Answer"), FieldStyleFactory::DefaultStyle );
+ m_fields << new Field( tr("Example"), "Example" );
+
+ if( records.isEmpty() )
+ records << new DicRecord();
+
+ CardPack* pack;
+ QList<const Field*> ansFields;
+
+ // Question->Answer
+ pack = new CardPack( this );
+ pack->setQstField( m_fields[0] );
+ ansFields << m_fields[1] << m_fields[2];
+ pack->setAnsFields( ansFields );
+ addCardPack( pack );
+
+ // Answer->Question
+ ansFields.clear();
+ pack = new CardPack( this );
+ pack->setQstField( m_fields[1] );
+ ansFields << m_fields[0] << m_fields[2];
+ pack->setAnsFields( ansFields );
+ addCardPack( pack );
+}
+
+bool Dictionary::load(const QString filePath)
+{
+ if(!loadDictFile(filePath))
+ return false;
+ generateCards();
+ cleanObsoleteId();
+ QString studyFilePath = getStudyFilePath();
+ if(!QFile::exists(studyFilePath))
+ return true;
+ return loadStudyFile(studyFilePath);
+}
+
+bool Dictionary::loadDictFile(const QString filePath)
+{
+ cleanRecords();
+ QFile dicFile(filePath);
+ if(!dicFile.open(QIODevice::ReadOnly | QFile::Text))
+ {
+ errorMessage = tr("Cannot open dictionary file:") +
+ QString("<p>%1</p>").arg(QDir::toNativeSeparators(filePath));
+ return false;
+ }
+ this->filePath = filePath;
+ DictionaryReader dicReader(this);
+ bool ok = dicReader.read( &dicFile );
+ dicFile.close();
+ if(!ok)
+ {
+ errorMessage = dicReader.errorString();
+ return false;
+ }
+
+ if(m_contentModified)
+ {
+ if(save() != QFile::NoError)
+ return false;
+ }
+ return true;
+}
+
+void Dictionary::cleanRecords()
+{
+ while (!records.isEmpty())
+ delete records.takeFirst();
+}
+
+void Dictionary::cleanObsoleteId()
+{
+ if(!obsoleteId.isNull())
+ obsoleteId = QUuid();
+}
+
+bool Dictionary::loadStudyFile(const QString filePath)
+{
+ QFile studyFile(filePath);
+ if(!studyFile.open(QIODevice::ReadOnly | QFile::Text))
+ {
+ errorMessage = tr("Cannot open study file:") +
+ QString("<p>%1</p>").arg(QDir::toNativeSeparators(filePath));
+ return false;
+ }
+ StudyFileReader studyReader( this );
+ bool ok = studyReader.read( &studyFile );
+ studyFile.close();
+ if(!ok)
+ errorMessage = studyReader.errorString() + QString(" at %1:%2")
+ .arg( studyReader.lineNumber() )
+ .arg( studyReader.columnNumber() );
+
+ if(m_studyModified)
+ {
+ if(saveStudy() != QFile::NoError)
+ return false;
+ }
+ return true;
+}
+
+QFile::FileError Dictionary::save( const QString aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = saveContent( aFilePath );
+ if(error != QFile::NoError)
+ return error;
+
+ if(aChangeFilePath && aFilePath != filePath)
+ {
+ filePath = aFilePath;
+ m_studyModified = true;
+ emit filePathChanged();
+ }
+
+ if(m_studyModified)
+ {
+ error = saveStudy();
+ if(error != QFile::NoError)
+ return error;
+ }
+
+ return QFile::NoError;
+}
+
+QFile::FileError Dictionary::saveContent( const QString aFilePath )
+ {
+ QFile file( aFilePath );
+ if( !file.open( QIODevice::WriteOnly | QFile::Text ) )
+ return file.error();
+ DictionaryWriter writer( this );
+ writer.write( &file );
+ file.close();
+ setContentModified( false );
+ m_nameIsTemp = false;
+ return QFile::NoError;
+ }
+
+QFile::FileError Dictionary::saveStudy()
+ {
+ if(!m_studyModified)
+ return QFile::NoError;
+ QFile file( getStudyFilePath() );
+ if(!file.open(QIODevice::WriteOnly | QFile::Text))
+ return file.error();
+ StudyFileWriter writer(this);
+ writer.write(&file);
+ file.close();
+ setStudyModified(false);
+ return QFile::NoError;
+ }
+
+QString Dictionary::shortName( bool aMarkModified ) const
+{
+QString fileName;
+if( !filePath.isEmpty() )
+ fileName = QFileInfo(filePath).fileName();
+else
+ fileName = tr(NoName) + DictFileExtension;
+if(aMarkModified && m_contentModified)
+ fileName += "*";
+return fileName;
+}
+
+QString Dictionary::getStudyFilePath() const
+{
+QString studyFilePath;
+if(obsoleteId.isNull())
+ {
+ // Take study file from the same dictionary directory
+ QFileInfo fileInfo(filePath);
+ studyFilePath = fileInfo.path() + "/" + fileInfo.completeBaseName() + StudyFileExtension;
+ }
+else
+ {
+ // Old dictionary. Take study file from the user settings directory
+ QSettings settings;
+ QFileInfo settingsInfo( settings.fileName() );
+ studyFilePath = settingsInfo.path() + "/study/" + obsoleteId.toString() + StudyFileExtension;
+ }
+return studyFilePath;
+}
+
+void Dictionary::setContentModified( bool aModified )
+{
+if( aModified != m_contentModified ) // The Content Modified state is changed
+ {
+ m_contentModified = aModified;
+ emit contentModifiedChanged( m_contentModified );
+ }
+}
+
+void Dictionary::setStudyModified(bool aModified)
+{
+if(aModified != m_studyModified)
+ {
+ m_studyModified = aModified;
+ emit studyModifiedChanged(m_studyModified);
+ }
+}
+
+const DicRecord* Dictionary::getRecord(int aIndex) const
+{
+ return records.value( aIndex );
+}
+
+const DicRecord* Dictionary::entry04( const QString aId04 ) const
+ {
+ if( aId04.isEmpty() )
+ return NULL;
+ foreach( DicRecord* entry, records )
+ if( entry->id04() == aId04 )
+ return entry;
+ return NULL;
+ }
+
+void Dictionary::setFieldValue( int aEntryIx, int aFieldIx, QString aValue )
+ {
+ DicRecord* entry = records.value(aEntryIx);
+ if( !entry )
+ return;
+ const Field* field = m_fields[aFieldIx];
+ if( field->name().isNull() )
+ return;
+ entry->setField( field->name(), aValue );
+ setContentModified();
+ emit entryChanged( aEntryIx, aFieldIx );
+ }
+
+ /// @return -1, if the given ID 0.4 was not found
+int Dictionary::fieldId04ToIx( const QString aId ) const
+ {
+ if( aId.isEmpty() )
+ return -1;
+ int i=0;
+ foreach( Field* f, m_fields )
+ {
+ if( f->id04() == aId )
+ return i;
+ i++;
+ }
+ return -1;
+ }
+
+const Field* Dictionary::field( const QString aFieldName ) const
+ {
+ foreach( Field* f, m_fields )
+ if( f->name() == aFieldName )
+ return f;
+ return NULL;
+ }
+
+QStringList Dictionary::fieldNames() const
+ {
+ QStringList names;
+ foreach( Field* f, m_fields )
+ names << f->name();
+ return names;
+ }
+
+CardPack* Dictionary::cardPack( QString aId ) const
+ {
+ foreach( CardPack* pack, m_cardPacks )
+ {
+ if( pack->id() == aId )
+ return pack;
+ }
+ return NULL;
+ }
+
+void Dictionary::setFieldName( int aField, QString aName )
+ {
+ if( aField >= m_fields.size() )
+ return;
+ m_fields[aField]->setName(aName);
+ setContentModified();
+ emit fieldChanged( aField );
+ }
+
+void Dictionary::setFieldStyle( int aField, QString aStyle )
+ {
+ if( aField >= m_fields.size() )
+ return;
+ m_fields[aField]->setStyle(aStyle);
+ setContentModified();
+ emit fieldChanged( aField );
+ }
+
+void Dictionary::insertField( int aPos, QString aName )
+ {
+ if( aPos > m_fields.size() )
+ return;
+ m_fields.insert( aPos, new Field(aName) );
+ setContentModified();
+ emit fieldInserted( aPos );
+ }
+
+void Dictionary::insertField( int aPos, Field* aFieldPtr )
+ {
+ if( aPos > m_fields.size() )
+ return;
+ m_fields.insert( aPos, aFieldPtr );
+ setContentModified();
+ emit fieldInserted( aPos );
+ }
+
+void Dictionary::addField( QString aName, QString aStyle )
+ {
+ m_fields << new Field( aName, aStyle );
+ setContentModified();
+ emit fieldInserted( m_fields.size()-1 );
+ }
+
+// TODO: Make undo command
+void Dictionary::addFields( QStringList aFieldNames )
+ {
+ foreach( QString aName, aFieldNames )
+ addField( aName );
+ }
+
+/// Just removes field pointer from list
+void Dictionary::removeField( int aPos )
+ {
+ if( aPos >= m_fields.size() )
+ return;
+ m_fields.removeAt( aPos );
+ setContentModified();
+ emit fieldRemoved( aPos );
+ }
+
+/// Removes the field pointer and destroys the field itself!
+void Dictionary::destroyField( int aPos )
+ {
+ if( aPos >= m_fields.size() )
+ return;
+
+ Field* removedField = m_fields.takeAt( aPos );
+ setContentModified();
+
+ // Remove this field in all packs
+ foreach( CardPack* pack, m_cardPacks )
+ pack->removeField( removedField );
+
+ delete removedField;
+ emit fieldRemoved( aPos );
+ emit fieldDestroyed( removedField );
+ }
+
+void Dictionary::insertPack( int aPos, CardPack* aPack )
+ {
+ if( aPos > m_cardPacks.size() )
+ return;
+ m_cardPacks.insert( aPos, aPack );
+ connect( aPack, SIGNAL(studyRecordAdded()), SLOT(setStudyModified()) );
+ connect( aPack, SIGNAL(cardsGenerated()), SIGNAL(cardsGenerated()) );
+ emit packInserted( aPos );
+ }
+
+
+void Dictionary::addCardPack( CardPack* aCardPack )
+ {
+ insertPack( m_cardPacks.size(), aCardPack );
+ }
+
+/// Just removes pack pointer from list
+void Dictionary::removePack( int aPos )
+ {
+ if( aPos >= m_cardPacks.size() )
+ return;
+ m_cardPacks.removeAt( aPos );
+ emit packRemoved( aPos );
+ }
+
+/// Removes the field pointer and destroys the field itself!
+void Dictionary::destroyPack( int aPos )
+ {
+ if( aPos >= m_cardPacks.size() )
+ return;
+ CardPack* removedPack = m_cardPacks.takeAt( aPos );
+ delete removedPack;
+ emit packRemoved( aPos );
+ emit packDestroyed( removedPack );
+ }
+
+void Dictionary::setRecord( int aIndex, const DicRecord& aRecord )
+ {
+ if( aIndex < 0 || aIndex >= records.size() )
+ return;
+ DicRecord* newRecord = new DicRecord( aRecord );
+ delete records.value( aIndex ); // delete old record
+ records.replace( aIndex, newRecord );
+ setContentModified();
+ emit entryChanged( aIndex, AllFields );
+ }
+
+void Dictionary::insertEntry(int aIndex, DicRecord* aEntry)
+ {
+ records.insert( aIndex, aEntry );
+ notifyRecordsInserted(aIndex, 1);
+ }
+
+void Dictionary::insertEntries( int aIndex, int aNum )
+ {
+ for(int i=0; i < aNum; i++)
+ records.insert( aIndex + i, new DicRecord() );
+ notifyRecordsInserted(aIndex, aNum);
+ }
+
+void Dictionary::insertEntries( int aIndex, QList<DicRecord*> aEntries )
+ {
+ int i = 0;
+ foreach( DicRecord* entry, aEntries )
+ {
+ records.insert( aIndex + i, entry );
+ i++;
+ }
+ notifyRecordsInserted(aIndex, aEntries.size());
+ }
+
+void Dictionary::notifyRecordsInserted(int index, int num)
+{
+ setContentModified();
+ emit entriesInserted(index, num);
+}
+
+void Dictionary::removeRecords(int aIndex, int aNum)
+ {
+ Q_ASSERT( aIndex + aNum <= records.size() );
+ for( int i=0; i < aNum; i++ )
+ delete records.takeAt( aIndex );
+ setContentModified();
+ emit entriesRemoved( aIndex, aNum );
+ }
+
+void Dictionary::removeRecord( QString aQuestion )
+ {
+ QMutableListIterator<DicRecord*> it( records );
+ int i = 0;
+ int removedIndex = -1; // First removed record
+ while( it.hasNext() )
+ {
+ DicRecord* record = it.next();
+ foreach( QString fieldStr, record->getFields() )
+ {
+ QStringList elements = fieldStr.split( CardPack::SynonymDelimiter, QString::SkipEmptyParts );
+ if( elements.contains( aQuestion ) )
+ {
+ it.remove();
+ if( removedIndex < 0 )
+ removedIndex = i;
+ break;
+ }
+ }
+ i++;
+ }
+ if( removedIndex >= 0 ) // if something was removed
+ {
+ setContentModified();
+ emit entriesRemoved( removedIndex, 1 );
+ }
+ }
+
+void Dictionary::generateCards()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->generateQuestions();
+ }
+
+void Dictionary::disableRecordUpdates()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->disableDictRecordUpdates();
+ }
+
+void Dictionary::enableRecordUpdates()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->enableDictRecordUpdates();
+ generateCards();
+ }
+
+QString Dictionary::shortenImagePaths(QString text) const
+{
+ QRegExp imgRx(QString("(<img\\s+src=\")%1([/\\\\])").arg(getImagesPath()));
+ text.replace(imgRx, "\\1%%\\2");
+ return text;
+}
+
+QString Dictionary::getFieldValue(int recordNum, int fieldNum) const
+{
+ QString fieldName = field(fieldNum)->name();
+ return getRecord(recordNum)-> getFieldValue(fieldName);
+}
diff --git a/src/dictionary/Dictionary.h b/src/dictionary/Dictionary.h
new file mode 100644
index 0000000..5cbeaa2
--- /dev/null
+++ b/src/dictionary/Dictionary.h
@@ -0,0 +1,187 @@
+#ifndef DICTIONARY_H
+#define DICTIONARY_H
+
+#include <QObject>
+#include <QList>
+#include <QString>
+#include <QFileInfo>
+#include <QFile>
+#include <QPair>
+#include <QUuid>
+
+#include "../export-import/CsvData.h"
+#include "IDictionary.h"
+#include "Card.h"
+#include "Field.h"
+#include "TreeItem.h"
+
+class DicRecord;
+class CardPack;
+class AppModel;
+
+/** \brief The dictionary
+ *
+ * The dictionary consists of entries #iEntries.
+ * The property #iIsContentModified shows if the dictionary contents are modified and not saved. Different modifications are possible:
+ * \li A single card was modified. Means contents of a card were modified. Signal CardModified(int) is emitted.
+ * \li The Content Modified state is changed. Means that iIsContentModified has changed. Signal ContentModifiedChanged(bool) is emitted.
+ * \li Some cards were inserted or removed. Independently of whether the cards are valid, signal
+ * CardsInserted(int, int) or CardsRemoved(int,int) is emitted.
+ * \li The file path is modified. Means that the dictionary file was renamed, e.g. after "Save as" operation.
+ * Signal filePathChanged() is emitted.
+ *
+ * All card-modifying functions and signals call SetModified().
+ */
+
+class Dictionary: public TreeItem, public IDictionary
+{
+Q_OBJECT
+
+friend class DictionaryReader;
+
+public:
+
+ // Methods
+ Dictionary( const QString& aFilePath = "", bool aNameIsTemp = false, const AppModel* aAppModel = NULL );
+ ~Dictionary();
+
+ const TreeItem* parent() const { return NULL; }
+ const TreeItem* child( int aRow ) const;
+ int childCount() const { return cardPacksNum(); }
+ int columnCount() const { return 1; }
+ QVariant data( int aColumn ) const;
+ int row() const;
+ int topParentRow() const { return row(); }
+
+ void clearFieldPackConfig();
+ void setDictConfig( const Dictionary* aOtherDic );
+ void setDefaultFields();
+ bool load(const QString aFilePath);
+ QFile::FileError save(const QString aFilePath, bool aChangeFilePath = true );
+ QFile::FileError save() { return save( filePath ); }
+ QFile::FileError saveContent( const QString aFilePath );
+ QFile::FileError saveStudy();
+
+ QString getFilePath() const { return filePath; }
+ QString shortName( bool aMarkModified = true ) const;
+ QString getStudyFilePath() const;
+ QString getErrorMessage() const { return errorMessage; }
+ bool contentModified() const { return m_contentModified; }
+ bool studyModified() const { return m_studyModified; }
+ bool nameIsTemp() const { return m_nameIsTemp; }
+ bool empty() const { return records.isEmpty(); }
+
+ QList<Field*> fields() const { return m_fields; }
+ const Field* field( int aIx ) const { return m_fields.value( aIx ); }
+ const Field* field( const QString aFieldName ) const;
+ QString getFieldValue(int recordNum, int fieldNum) const;
+ int fieldsNum() const { return m_fields.size(); }
+ QStringList fieldNames() const;
+
+ int cardPacksNum() const { return m_cardPacks.size(); }
+ QList<CardPack*> cardPacks() const { return m_cardPacks; }
+ CardPack* cardPack( int aIx ) const {return m_cardPacks.value(aIx);}
+ CardPack* cardPack( QString aId ) const;
+ int indexOfCardPack( CardPack* aPack ) const { return m_cardPacks.indexOf( aPack ); }
+
+ int fieldId04ToIx( const QString aId ) const; // Obsolete
+ const DicRecord* getRecord( int aIndex ) const;
+ const DicRecord* entry04( const QString aId04 ) const; // Obsolete
+ const DicRecord* operator[]( int aIndex ) const { return getRecord(aIndex); }
+ int entriesNum() const { return records.size(); }
+ int indexOfRecord( DicRecord* aRecord ) const { return records.indexOf( aRecord ); }
+
+ void setAppModel( const AppModel* aModel ) { Q_ASSERT( !m_appModel ); m_appModel = aModel; }
+
+ // Modify fields
+
+ void setFieldName( int aField, QString aName );
+ void setFieldStyle( int aField, QString aStyle );
+ void insertField( int aPos, QString aName );
+ void insertField( int aPos, Field* aFieldPtr );
+ void addField( QString aName, QString aStyle = "" );
+ void addFields( QStringList aFieldNames );
+ void removeField( int aPos );
+ void destroyField( int aPos );
+
+ // Modify card packs
+
+ void insertPack( int aPos, CardPack* aPack );
+ void removePack( int aPos );
+ void destroyPack( int aPos );
+ void addCardPack( CardPack* aCardPack );
+
+ // Modify records
+
+ void setFieldValue( int aEntryIx, int aFieldIx, QString aValue );
+ void setRecord( int aIndex, const DicRecord& aRecord );
+ void insertEntry(int aIndex, DicRecord* aEntry);
+ void insertEntries(int aIndex, int aNum);
+ void insertEntries( int aIndex, QList<DicRecord*> aEntries );
+ void removeEntry(int aIndex) { removeRecords( aIndex, 1 ); }
+ void cleanRecords();
+ void removeRecords(int aIndex, int aNum);
+ void removeRecord(QString aQuestion);
+ void disableRecordUpdates();
+ void enableRecordUpdates();
+
+ QString shortenImagePaths(QString text) const;
+
+signals:
+ void entryChanged( int aEntryIx, int aFieldIx );
+ void entriesRemoved( int aIndex, int aNum );
+ void entriesInserted( int aIndex, int aNum );
+
+ void fieldChanged( int aField );
+ void fieldInserted( int aField );
+ void fieldRemoved( int aField );
+ void fieldDestroyed( Field* aField );
+
+ void packInserted( int aPack );
+ void packRemoved( int aPack );
+ void packDestroyed( CardPack* aPack );
+ void contentModifiedChanged( bool aModified );
+ void studyModifiedChanged( bool aModified );
+
+ void filePathChanged();
+ void cardsGenerated();
+
+public slots:
+ void setContentModified( bool aModified = true );
+ void setContentClean( bool aClean = true ) { setContentModified( !aClean ); }
+ void setStudyModified( bool aModified = true );
+ void generateCards();
+
+private:
+ bool loadDictFile(const QString filePath);
+ bool loadStudyFile(const QString filePath);
+ void cleanObsoleteId();
+
+protected:
+ void notifyRecordsInserted(int index, int num);
+
+public:
+ static const char* NoName;
+ static const QString DictFileExtension;
+ static const QString StudyFileExtension;
+
+private:
+ const AppModel* m_appModel;
+ QUuid obsoleteId; // Obsolete starting from 1.0
+ QList<Field*> m_fields;
+
+ bool m_contentModified;
+ bool m_studyModified;
+
+ /**
+ The current file name is temporary and must be changed at the next save.
+ Use cases:
+ \li the dictionary has just been imported from a CSV file
+ \li the dictionary of obsolete format was converted to the modern format
+ */
+ bool m_nameIsTemp;
+
+ QString errorMessage;
+};
+
+#endif
diff --git a/src/dictionary/DictionaryReader.cpp b/src/dictionary/DictionaryReader.cpp
new file mode 100644
index 0000000..8a3391c
--- /dev/null
+++ b/src/dictionary/DictionaryReader.cpp
@@ -0,0 +1,384 @@
+#include "DictionaryReader.h"
+#include "Dictionary.h"
+#include "../version.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QUuid>
+#include <QMessageBox>
+
+const QString DictionaryReader::MinSupportedDictVersion = "0.3";
+
+DictionaryReader::DictionaryReader( Dictionary* aDict ):
+ m_dict( aDict )
+ {
+
+ }
+
+bool DictionaryReader::read( QIODevice* aDevice )
+ {
+ setDevice( aDevice );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isStartElement() )
+ {
+ if( name() == "dict" )
+ readDict();
+ else
+ raiseError( Dictionary::tr("The file is not a dictionary file.") );
+ }
+ }
+ return !error();
+ }
+
+void DictionaryReader::readDict()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict" );
+
+ m_dict->m_contentModified = false;
+ QString id = attributes().value( "id" ).toString();
+ if(!id.isEmpty()) // Id is obsolete. If it exists, write it down just for loading the study file.
+ m_dict->obsoleteId = QUuid(id);
+
+ m_dictVersion = attributes().value( "version" ).toString();
+ if( m_dictVersion == DIC_VERSION)
+ readDictCurrentVersion();
+ else if( m_dictVersion == "1.0" )
+ {
+ notifyObsoleteVersion(m_dictVersion);
+ readDictCurrentVersion();
+ }
+ else if( m_dictVersion == "0.4" )
+ {
+ notifyObsoleteVersion( m_dictVersion );
+ readDict04();
+ }
+ else if( m_dictVersion == "0.3" )
+ {
+ notifyObsoleteVersion( m_dictVersion );
+ readDict03();
+ }
+ else
+ {
+ QMessageBox::warning( NULL, Dictionary::tr("Unsupported format"),
+ Dictionary::tr("Dictionary uses unsupported format %1.\n"
+ "The minimum supported version is %2" )
+ .arg( m_dictVersion )
+ .arg( MinSupportedDictVersion ) );
+ }
+ }
+
+void DictionaryReader::notifyObsoleteVersion( const QString& aOldVersion )
+ {
+ QMessageBox::warning( NULL, Dictionary::tr("Old dictionary"),
+ Dictionary::tr("Dictionary %1 uses obsolete format %2.\n"
+ "It will be converted to the current format %3." )
+ .arg(m_dict->shortName(false))
+ .arg(aOldVersion).arg(DIC_VERSION));
+ m_dict->m_contentModified = true;
+ }
+
+void DictionaryReader::readUnknownElement()
+ {
+ Q_ASSERT( isStartElement() );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ readUnknownElement();
+ }
+ }
+
+void DictionaryReader::readDictCurrentVersion()
+ {
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "fields" )
+ readFields();
+ else if( name() == "packs" )
+ readPacks();
+ else if( name() == "entries" )
+ readEntries();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readFields()
+ {
+ Q_ASSERT( isStartElement() && name() == "fields" );
+
+ if( m_dictVersion == "0.4" )
+ {
+ m_curCardPack = new CardPack( m_dict );
+ m_curAnsFieldList.clear();
+ }
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "field" )
+ readField();
+ else
+ readUnknownElement();
+ }
+ }
+
+ if( m_dictVersion == "0.4" && m_curCardPack )
+ {
+ m_curCardPack->setAnsFields( m_curAnsFieldList );
+ m_dict->m_cardPacks << m_curCardPack;
+
+ // And create the reversed pack: qst and ans swapped
+ CardPack* pack2 = new CardPack( m_dict );
+ pack2->setQstField( m_curCardPack->getAnswerFields()[0] );
+ QList<const Field*> fList2;
+ fList2 << m_curCardPack->getQuestionField(); // First answer
+ for( int i=1; i<m_curCardPack->getAnswerFields().size(); i++ ) // Other answers
+ fList2 << m_curCardPack->getAnswerFields()[i];
+ pack2->setAnsFields( fList2 );
+ m_dict->m_cardPacks << pack2;
+ }
+ }
+
+void DictionaryReader::readField()
+ {
+ Q_ASSERT( isStartElement() && name() == "field" );
+
+ QString qstAttr;
+ QString ansAttr;
+ QString id04Attr;
+ if( m_dictVersion == "0.4" )
+ {
+ qstAttr = attributes().value("question").toString();
+ ansAttr = attributes().value("answer").toString();
+ id04Attr = attributes().value("id").toString();
+ }
+
+ QString style = attributes().value("style").toString();
+ if( style.isEmpty() )
+ style = FieldStyleFactory::DefaultStyle;
+ QString fieldName = readElementText().trimmed();
+ Field* field = new Field( fieldName, style );
+
+ m_dict->m_fields << field; // insert in order of occurence
+
+ if( m_dictVersion == "0.4" )
+ {
+ Field* field = m_dict->m_fields.last();
+ if( !id04Attr.isEmpty() )
+ field->setId04( id04Attr );
+ if( qstAttr == "yes" && m_curCardPack )
+ m_curCardPack->setQstField( field );
+ else
+ {
+ bool ok;
+ int ansIx = ansAttr.toInt( &ok );
+ if( ok )
+ m_curAnsFieldList.insert( ansIx, field );
+ }
+ }
+
+ }
+
+void DictionaryReader::readPacks()
+ {
+ Q_ASSERT( isStartElement() && name() == "packs" );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "pack" )
+ readPack();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readPack()
+ {
+ Q_ASSERT( isStartElement() && name() == "pack" );
+ CardPack* cardPack = new CardPack( m_dict );
+ QString exactAnswer = attributes().value("exact-ans").toString();
+ if(exactAnswer == "true")
+ cardPack->setUsesExactAnswer(true);
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "qst" || name() == "ans" )
+ {
+ QString fieldName = readElementText().trimmed();
+ const Field* field = m_dict->field( fieldName );
+ if( !field )
+ continue;
+ if( name() == "qst" )
+ cardPack->setQstField( field );
+ else // ans
+ cardPack->addField( field );
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->addCardPack( cardPack );
+ }
+
+void DictionaryReader::readEntries()
+ {
+ Q_ASSERT( isStartElement() && name() == "entries" );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "e" )
+ readE();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+/** Read entry */
+void DictionaryReader::readE()
+ {
+ DicRecord* dicEntry = new DicRecord;
+ if( m_dictVersion == "0.4" )
+ {
+ QString idStr = attributes().value("id").toString();
+ dicEntry->setId04( idStr );
+ }
+
+ int curFieldIx = 0;
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "f" )
+ {
+ if( m_dictVersion == "0.4" )
+ {
+ QString fieldId04 = attributes().value("id").toString();
+ int ix = m_dict->fieldId04ToIx( fieldId04 );
+ if( ix > -1 ) // found
+ curFieldIx = ix;
+ }
+ QString fieldValue = readElementText().trimmed();
+ const Field* field = m_dict->field( curFieldIx );
+ if( !field )
+ break; // no more fields
+ dicEntry->setField( field->name(), fieldValue );
+ curFieldIx++;
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->records << dicEntry;
+ }
+
+void DictionaryReader::readDict04()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict"
+ && attributes().value("version") == "0.4" );
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "fields" )
+ readFields();
+ else if( name() == "c" )
+ readE();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readDict03()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict"
+ && attributes().value("version") == "0.3" );
+
+ // Read entries
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "card" )
+ readE03();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+/** Read entry v.0.3*/
+void DictionaryReader::readE03()
+ {
+ Q_ASSERT( isStartElement() && m_dictVersion == "0.3" && name() == "card" );
+ DicRecord* dicEntry = new DicRecord;
+
+ QMap<QString, QString> fieldNames; // tag name, field name
+ fieldNames["qst"] = Dictionary::tr("Question");
+ fieldNames["ans"] = Dictionary::tr("Answer");
+ fieldNames["xmp"] = Dictionary::tr("Example");
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ QString tagName = name().toString();
+ if( fieldNames.contains( tagName ) )
+ {
+ QString fieldValue = readElementText().trimmed();
+ dicEntry->setField( fieldNames.value( tagName ), fieldValue );
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->records << dicEntry;
+ }
diff --git a/src/dictionary/DictionaryReader.h b/src/dictionary/DictionaryReader.h
new file mode 100644
index 0000000..ca2ff00
--- /dev/null
+++ b/src/dictionary/DictionaryReader.h
@@ -0,0 +1,44 @@
+#ifndef DICTIONARYREADER_H
+#define DICTIONARYREADER_H
+
+#include <QXmlStreamReader>
+#include <QList>
+#include <QString>
+
+class Dictionary;
+class CardPack;
+class Field;
+
+class DictionaryReader : public QXmlStreamReader
+{
+public:
+ DictionaryReader( Dictionary* aDict );
+ bool read( QIODevice* aDevice );
+
+private:
+ static const QString MinSupportedDictVersion;
+
+private:
+ void readDict();
+ void notifyObsoleteVersion( const QString& aOldVersion );
+ void readUnknownElement();
+ void readDictCurrentVersion();
+ void readFields();
+ void readField();
+ void readPacks();
+ void readPack();
+ void readEntries();
+ void readE();
+ void readDict04();
+ void readDict03();
+ void readE03();
+
+private:
+ Dictionary* m_dict;
+ QString m_dictVersion;
+ CardPack* m_curCardPack; ///< For 0.4
+ QList<const Field*> m_curAnsFieldList; ///< For 0.4
+
+};
+
+#endif // DICTIONARYREADER_H
diff --git a/src/dictionary/DictionaryWriter.cpp b/src/dictionary/DictionaryWriter.cpp
new file mode 100644
index 0000000..14cd087
--- /dev/null
+++ b/src/dictionary/DictionaryWriter.cpp
@@ -0,0 +1,79 @@
+#include "DictionaryWriter.h"
+#include "Dictionary.h"
+#include "../version.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+
+DictionaryWriter::DictionaryWriter( const Dictionary* aDict ):
+ m_dict( aDict )
+{
+ setAutoFormatting( true );
+}
+
+bool DictionaryWriter::write( QIODevice* aDevice )
+{
+ setDevice( aDevice );
+ writeStartDocument();
+ writeDTD( "<!DOCTYPE freshmemory-dict>" );
+ writeStartElement("dict");
+ writeAttribute( "version", DIC_VERSION );
+
+ writeStartElement( "fields" );
+ foreach( Field *field, m_dict->fields() )
+ writeField( field );
+ writeEndElement();
+
+ writeStartElement( "packs" );
+ foreach( const CardPack* pack, m_dict->cardPacks() )
+ writePack( pack );
+ writeEndElement();
+
+ writeStartElement( "entries" );
+ foreach( const DicRecord* entry, m_dict->getRecords() )
+ writeEntry( entry, m_dict->fields() );
+ writeEndElement();
+
+ writeEndDocument();
+ return true;
+}
+
+void DictionaryWriter::writeField( Field* aField )
+ {
+ writeStartElement( "field" );
+ QString style = aField->style();
+ if( !style.isEmpty() && style != FieldStyleFactory::DefaultStyle )
+ writeAttribute( "style", style );
+ writeCharacters( aField->name() );
+ writeEndElement();
+ }
+
+void DictionaryWriter::writePack( const CardPack* aPack )
+ {
+ writeStartElement( "pack" );
+ if(aPack->getUsesExactAnswer())
+ writeAttribute( "exact-ans", "true" );
+ writeTextElement( "qst", aPack->getQuestionField()->name() );
+ foreach( const Field* field, aPack->getAnswerFields() )
+ writeTextElement( "ans", field->name() );
+ writeEndElement();
+ }
+
+void DictionaryWriter::writeEntry( const DicRecord* aEntry, QList<Field*> aFields )
+ {
+ writeStartElement( "e" );
+ int emptyFieldsNum = 0;
+ foreach( Field* field, aFields )
+ {
+ QString fieldValue = aEntry->getFieldValue( field->name() );
+ if( fieldValue.isEmpty() )
+ emptyFieldsNum++;
+ else
+ {
+ for( int k = 0; k < emptyFieldsNum; k++ )
+ writeEmptyElement( "f" );
+ emptyFieldsNum = 0;
+ writeTextElement( "f", fieldValue );
+ }
+ }
+ writeEndElement();
+ }
diff --git a/src/dictionary/DictionaryWriter.h b/src/dictionary/DictionaryWriter.h
new file mode 100644
index 0000000..82050a4
--- /dev/null
+++ b/src/dictionary/DictionaryWriter.h
@@ -0,0 +1,26 @@
+#ifndef DICTIONARYWRITER_H
+#define DICTIONARYWRITER_H
+
+#include <QXmlStreamWriter>
+
+class Dictionary;
+class Field;
+class CardPack;
+class DicRecord;
+
+class DictionaryWriter : public QXmlStreamWriter
+{
+public:
+ DictionaryWriter( const Dictionary* aDict );
+ bool write( QIODevice* aDevice );
+
+private:
+ void writeField( Field* aField );
+ void writePack( const CardPack* aPack );
+ void writeEntry( const DicRecord* aEntry, QList<Field*> aFields );
+
+private:
+ const Dictionary* m_dict;
+};
+
+#endif // DICTIONARYWRITER_H
diff --git a/src/dictionary/Field.cpp b/src/dictionary/Field.cpp
new file mode 100644
index 0000000..5992ac0
--- /dev/null
+++ b/src/dictionary/Field.cpp
@@ -0,0 +1,18 @@
+#include "Field.h"
+
+Field::Field( QString aName, QString aStyle ):
+ m_name( aName )
+ {
+ if( aStyle.isEmpty() )
+ m_style = FieldStyleFactory::DefaultStyle;
+ else
+ m_style = aStyle;
+ }
+
+void Field::setName( const QString aName )
+ {
+ // Save the first original name
+ if( m_oldName.isEmpty() )
+ m_oldName = m_name;
+ m_name = aName;
+ }
diff --git a/src/dictionary/Field.h b/src/dictionary/Field.h
new file mode 100644
index 0000000..08b0123
--- /dev/null
+++ b/src/dictionary/Field.h
@@ -0,0 +1,46 @@
+#ifndef FIELD_H
+#define FIELD_H
+
+#include "../field-styles/FieldStyle.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QString>
+
+/**
+ Field is a field of the dictionary entries. The fields are owned by the dictionary.
+ Dictionary entries and card packs reuse the fields of the dictionary.
+
+ Field id is its name. It's unique.
+
+ When the field is renamed, its first original name is always saved in m_oldName.
+ */
+
+class Field
+{
+public:
+ Field() {}
+ Field( QString aName, QString aStyle = "" );
+
+public:
+ QString name() const {return m_name;}
+ QString oldName() const {return m_oldName;}
+ QString style() const {return m_style;}
+ bool operator<( const Field& anotherField ) const { return m_name < anotherField.name(); }
+ bool operator==( const Field& anotherField ) const { return m_name == anotherField.name(); }
+ QString id04() const { return m_id04; }
+
+ void setName( const QString aName );
+ void setStyle( QString aStyle ) { m_style = aStyle; }
+ void setId04( const QString& aId ) { m_id04 = aId; }
+
+private:
+ QString m_name;
+ QString m_style;
+ QString m_oldName; // For renaming
+
+ // Obsolete:
+ QString m_id04; ///< ID, compatible with v. 0.4
+
+};
+
+#endif
diff --git a/src/dictionary/ICardPack.cpp b/src/dictionary/ICardPack.cpp
new file mode 100644
index 0000000..d2ef03e
--- /dev/null
+++ b/src/dictionary/ICardPack.cpp
@@ -0,0 +1,23 @@
+#include "ICardPack.h"
+#include "Field.h"
+
+const QString ICardPack::SynonymDelimiter = ";";
+const QString ICardPack::HomonymDelimiter = "; ";
+
+QString ICardPack::getQuestionFieldName() const
+{
+ const Field* questionField = getQuestionField();
+ if(questionField)
+ return questionField->name();
+ else
+ return QString();
+}
+
+QStringList ICardPack::getAnswerFieldNames() const
+{
+ QStringList list;
+ foreach(const Field* field, getAnswerFields())
+ if(field)
+ list << field->name();
+ return list;
+}
diff --git a/src/dictionary/ICardPack.h b/src/dictionary/ICardPack.h
new file mode 100644
index 0000000..9c8e4a2
--- /dev/null
+++ b/src/dictionary/ICardPack.h
@@ -0,0 +1,33 @@
+#ifndef ICARDPACK_H
+#define ICARDPACK_H
+
+#include <QString>
+#include <QList>
+
+#include "../study/StudyRecord.h"
+
+class DicRecord;
+class Field;
+
+class ICardPack
+{
+public:
+ static const QString SynonymDelimiter;
+ static const QString HomonymDelimiter;
+
+public:
+ virtual ~ICardPack() {}
+
+ virtual void addStudyRecord(const QString aCardId,
+ const StudyRecord& aStudyRecord) = 0;
+ virtual QList<StudyRecord> getStudyRecords(QString cardId) const = 0;
+ virtual StudyRecord getStudyRecord(QString cardId) const = 0;
+
+ virtual QList<DicRecord*> getRecords() const = 0;
+ virtual const Field* getQuestionField() const = 0;
+ virtual QList<const Field*> getAnswerFields() const = 0;
+ QString getQuestionFieldName() const;
+ QStringList getAnswerFieldNames() const;
+};
+
+#endif
diff --git a/src/dictionary/IDictionary.cpp b/src/dictionary/IDictionary.cpp
new file mode 100644
index 0000000..20bac26
--- /dev/null
+++ b/src/dictionary/IDictionary.cpp
@@ -0,0 +1,59 @@
+#include "IDictionary.h"
+#include "DicRecord.h"
+#include "CardPack.h"
+
+IDictionary::~IDictionary()
+{
+ foreach(DicRecord* record, records)
+ delete record;
+}
+
+void IDictionary::addRecord(DicRecord* record)
+{
+ records << record;
+ notifyRecordsInserted(records.size() - 1, 1);
+}
+
+void IDictionary::addRecords(const QList<DicRecord*>& records)
+{
+ this->records << records;
+ notifyRecordsInserted(records.size() - records.size(), records.size());
+}
+
+QString IDictionary::extendImagePaths(QString text) const
+{
+ text = replaceImagePaths(text, "%", QFileInfo(filePath).path());
+ text = replaceImagePaths(text, "%%", getImagesPath());
+ return text;
+}
+
+QString IDictionary::getImagesPath() const
+{
+ QString path = QFileInfo(filePath).path();
+ QString baseName = QFileInfo(filePath).completeBaseName();
+ return QDir(path).filePath(baseName);
+}
+
+QString IDictionary::replaceImagePaths(QString text, const QString& shortDir,
+ const QString& replacingPath)
+{
+ QRegExp imgRx(QString("(<img src=\")%1([/\\\\])").arg(shortDir));
+ text.replace(imgRx, QString("\\1%1\\2").arg(replacingPath));
+ return text;
+}
+
+int IDictionary::countTodaysAllCards() const
+ {
+ int num = 0;
+ foreach(CardPack* pack, m_cardPacks)
+ num += pack->getTodayReviewedCardsNum();
+ return num;
+ }
+
+int IDictionary::countTodaysNewCards() const
+ {
+ int num = 0;
+ foreach(CardPack* pack, m_cardPacks)
+ num += pack->getTodayNewCardsNum();
+ return num;
+ }
diff --git a/src/dictionary/IDictionary.h b/src/dictionary/IDictionary.h
new file mode 100644
index 0000000..bfc82bc
--- /dev/null
+++ b/src/dictionary/IDictionary.h
@@ -0,0 +1,51 @@
+#ifndef IDICTIONARY_H
+#define IDICTIONARY_H
+
+#include <QtCore>
+
+class DicRecord;
+class Field;
+class CardPack;
+
+class IDictionary
+{
+public:
+ static const int AllFields = -1;
+
+public:
+ IDictionary(const QString& filePath = ""):
+ filePath(filePath) {}
+ virtual ~IDictionary();
+
+ void addRecord(DicRecord* record);
+ void addRecords(const QList<DicRecord*>& records);
+ QList<DicRecord*> getRecords() const { return records; }
+
+ virtual const Field* field( int aIx ) const = 0;
+ virtual const Field* field( const QString aFieldName ) const = 0;
+ virtual int indexOfCardPack( CardPack* aPack ) const = 0;
+
+ virtual void addCardPack(CardPack* aCardPack) = 0;
+
+ virtual QFile::FileError saveStudy() { return QFile::NoError; }
+
+ // Stats
+ int countTodaysAllCards() const;
+ int countTodaysNewCards() const;
+
+ QString extendImagePaths(QString text) const;
+ QString getImagesPath() const;
+
+protected:
+ virtual void notifyRecordsInserted(int /*index*/, int /*num*/) {}
+
+private:
+ static QString replaceImagePaths(QString text, const QString& shortDir,
+ const QString& replacingPath);
+
+protected:
+ QString filePath;
+ QList<DicRecord*> records;
+ QList<CardPack*> m_cardPacks;
+};
+#endif
diff --git a/src/dictionary/TreeItem.h b/src/dictionary/TreeItem.h
new file mode 100644
index 0000000..7c65cd0
--- /dev/null
+++ b/src/dictionary/TreeItem.h
@@ -0,0 +1,25 @@
+#ifndef TREEITEM_H
+#define TREEITEM_H
+
+#include <QVariant>
+
+class TreeItem: public QObject
+{
+public:
+ TreeItem() {}
+ virtual ~TreeItem() {}
+
+ virtual const TreeItem* parent() const = 0;
+ virtual const TreeItem* child( int aRow ) const = 0;
+ virtual int childCount() const = 0;
+ virtual int columnCount() const = 0;
+ virtual QVariant data( int aColumn ) const = 0;
+ virtual int row() const = 0;
+ virtual int topParentRow() const = 0;
+
+protected:
+ virtual void entryChanged(int, int) {};
+ virtual void entriesRemoved(int, int) {};
+};
+
+#endif // TREEITEM_H
diff --git a/src/export-import/CsvData.h b/src/export-import/CsvData.h
new file mode 100644
index 0000000..c6ea9ad
--- /dev/null
+++ b/src/export-import/CsvData.h
@@ -0,0 +1,36 @@
+#ifndef CSVDATA_H
+#define CSVDATA_H
+
+#include <QtCore>
+
+enum FieldSeparationMode
+{
+ EFieldSeparatorAnyCharacter = 0,
+ EFieldSeparatorAnyCombination,
+ EFieldSeparatorExactString
+};
+
+struct CsvParams
+{
+ QString fieldSeparators;
+ QChar textDelimiter;
+ QChar commentChar;
+};
+
+struct CsvImportData: public CsvParams
+{
+ QTextCodec* textCodec;
+ int fromLine;
+ bool firstLineIsHeader;
+ FieldSeparationMode fieldSeparationMode;
+ int colsToImport; // 0 = All columns
+};
+
+struct CsvExportData: public CsvParams
+{
+ QList<int> usedCols; // Empty = use all columns
+ bool quoteAllFields;
+ bool writeColumnNames;
+};
+
+#endif
diff --git a/src/export-import/CsvDialog.cpp b/src/export-import/CsvDialog.cpp
new file mode 100644
index 0000000..b19d029
--- /dev/null
+++ b/src/export-import/CsvDialog.cpp
@@ -0,0 +1,176 @@
+#include "CsvDialog.h"
+
+QChar CsvDialog::SpaceChar(0x2423);
+QChar CsvDialog::TabChar(0x21A6);
+QString CsvDialog::ExtendedTab(QString(QChar(0x21A6)) + "\t");
+
+CsvDialog::CsvDialog(QWidget* parent):
+ QDialog(parent)
+{
+ resize(600, 550);
+}
+
+void CsvDialog::init()
+{
+ setLayout(createMainLayout());
+ updatePreview();
+ connectControlsToPreview();
+}
+
+void CsvDialog::connectControlsToPreview()
+{
+ connect(separatorsEdit, SIGNAL(textChanged(QString)), SLOT(updatePreview()) );
+ connect(textDelimiterCB, SIGNAL(stateChanged(int)), SLOT(updatePreview()) );
+ connect(textDelimiterCombo, SIGNAL(editTextChanged(QString)), SLOT(updatePreview()) );
+ connect(commentCharCombo, SIGNAL(editTextChanged(QString)), SLOT(updatePreview()) );
+}
+
+QLayout* CsvDialog::createMainLayout()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addLayout(createTopLayout());
+ lt->addWidget(new QLabel(tr("Preview:")));
+ lt->addLayout(createPreviewLt());
+ lt->addWidget(createButtonBox());
+ return lt;
+}
+
+QLayout* CsvDialog::createTopLayout()
+{
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(createLeftGroup());
+ lt->addWidget(createSeparatorsGroup());
+ return lt;
+}
+
+QWidget* CsvDialog::createCharSetCombo()
+{
+ charSetCombo = new QComboBox;
+ charSetCombo->setEditable(false);
+ QList<QByteArray> codecNames = QTextCodec::availableCodecs();
+ qSort( codecNames );
+ for( int i=0; i<codecNames.size(); i++)
+ charSetCombo->addItem(codecNames[i],
+ QTextCodec::codecForName(codecNames[i] )->mibEnum());
+ int utf8Ix = codecNames.indexOf( "UTF-8" );
+ charSetCombo->setCurrentIndex( utf8Ix );
+ return charSetCombo;
+}
+
+QGroupBox* CsvDialog::createLeftGroup()
+{
+ QGroupBox* group = new QGroupBox(getLeftGroupTitle());
+ group->setLayout(createLeftGroupLayout());
+ return group;
+}
+
+QGroupBox* CsvDialog::createSeparatorsGroup()
+{
+ QGroupBox* group = new QGroupBox(tr("Separators"));
+ group->setLayout(createSeparatorsLayout());
+ return group;
+}
+
+QDialogButtonBox* CsvDialog::createButtonBox()
+{
+ QDialogButtonBox* buttonBox = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ return buttonBox;
+}
+
+QWidget* CsvDialog::createFieldSeparatorWidget(const QString& additionalSeparators)
+{
+ separatorsEdit = new QLineEdit(QString(TabChar) + "&" + additionalSeparators);
+ connect(separatorsEdit, SIGNAL(textChanged(const QString&)),
+ SLOT(showInvisibleInSeparatorsEdit()));
+
+ QPushButton* addSeparatorBtn = new QPushButton(tr("Ta&b"));
+ addSeparatorBtn->setMinimumWidth(40);
+ connect(addSeparatorBtn, SIGNAL(clicked()), SLOT(insertTabToSeparators()));
+
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(separatorsEdit);
+ lt->addWidget(addSeparatorBtn);
+ lt->setContentsMargins(QMargins());
+
+ // Workaround for a bug in QFormLayout::addRow(text, layout)
+ QWidget* widget = new QWidget;
+ widget->setLayout(lt);
+ return widget;
+}
+
+QWidget* CsvDialog::createTextDelimiterBox()
+{
+ textDelimiterCB = new QCheckBox(tr("&Text delimiter:"));
+ textDelimiterCB->setChecked(true);
+ return textDelimiterCB;
+}
+
+QWidget* CsvDialog::createTextDelimiterCombo()
+{
+ return textDelimiterCombo = createEditableCombo(QStringList() << "\"" << "\'");
+}
+
+QWidget* CsvDialog::createCommentCharCombo()
+{
+ return commentCharCombo = createEditableCombo(QStringList() << "#" << ";");
+}
+
+QComboBox* CsvDialog::createEditableCombo(const QStringList& items)
+{
+ QComboBox* combo = new QComboBox;
+ combo->setEditable(true);
+ combo->setInsertPolicy(QComboBox::NoInsert);
+ combo->setValidator(new QRegExpValidator(QRegExp("\\S{1}"), this));
+ combo->addItems(items);
+ return combo;
+}
+
+QChar CsvDialog::getTextDelimiterChar() const
+{
+ if(textDelimiterCB->isChecked() && !textDelimiterCombo->currentText().isEmpty())
+ return textDelimiterCombo->currentText()[0];
+ else
+ return QChar(0);
+}
+
+void CsvDialog::showInvisibleInSeparatorsEdit()
+{
+ separatorsEdit->setText(setCharVisibility(separatorsEdit->text(), true));
+}
+
+void CsvDialog::insertTabToSeparators()
+{
+ separatorsEdit->insert(TabChar);
+}
+
+QTextCodec* CsvDialog::getTextCodec()
+{
+ return QTextCodec::codecForMib(
+ charSetCombo->itemData(charSetCombo->currentIndex()).toInt());
+}
+
+/// Convert invisible chars like space and tab to visible equivalents or vice versa.
+QString CsvDialog::setCharVisibility(const QString& input, bool visible)
+{
+ QString res = input;
+ if(visible)
+ {
+ res.replace(' ', SpaceChar);
+ res.replace('\t', ExtendedTab);
+ }
+ else
+ {
+ res.replace(SpaceChar, ' ');
+ res.replace(ExtendedTab, "\t");
+ res.replace(TabChar, '\t');
+ }
+ return res;
+}
+
+void CsvDialog::updateTextDelimiterCombo()
+{
+ textDelimiterCombo->setEnabled( textDelimiterCB->isChecked() );
+}
diff --git a/src/export-import/CsvDialog.h b/src/export-import/CsvDialog.h
new file mode 100644
index 0000000..f50cd62
--- /dev/null
+++ b/src/export-import/CsvDialog.h
@@ -0,0 +1,62 @@
+#ifndef CSV_DIALOG_H
+#define CSV_DIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class CsvDialog: public QDialog
+{
+ Q_OBJECT
+
+public:
+ CsvDialog(QWidget* parent);
+
+protected:
+ static QString setCharVisibility(const QString& input, bool visible);
+
+protected:
+ void init();
+ virtual QLayout* createLeftGroupLayout() = 0;
+ virtual QString getLeftGroupTitle() = 0;
+ QWidget* createCharSetCombo();
+ virtual QLayout* createSeparatorsLayout() = 0;
+ QWidget* createFieldSeparatorWidget(const QString& additionalSeparators = "");
+ QWidget* createTextDelimiterBox();
+ QWidget* createTextDelimiterCombo();
+ QWidget* createCommentCharCombo();
+ QComboBox* createEditableCombo(const QStringList& items);
+ virtual QLayout* createPreviewLt() = 0;
+ QDialogButtonBox* createButtonBox();
+ QTextCodec* getTextCodec();
+ QChar getTextDelimiterChar() const;
+
+protected slots:
+ virtual void updatePreview() = 0;
+ void updateTextDelimiterCombo();
+
+private:
+ QLayout* createMainLayout();
+ void connectControlsToPreview();
+ QLayout* createTopLayout();
+ QGroupBox* createLeftGroup();
+ QGroupBox* createSeparatorsGroup();
+ void doUpdatePreview() { updatePreview(); }
+
+private slots:
+ void showInvisibleInSeparatorsEdit();
+ void insertTabToSeparators();
+
+protected:
+ static QChar SpaceChar;
+ static QChar TabChar;
+ static QString ExtendedTab;
+
+protected:
+ QComboBox* charSetCombo;
+ QLineEdit* separatorsEdit;
+ QCheckBox* textDelimiterCB;
+ QComboBox* textDelimiterCombo;
+ QComboBox* commentCharCombo;
+};
+
+#endif
diff --git a/src/export-import/CsvExportDialog.cpp b/src/export-import/CsvExportDialog.cpp
new file mode 100644
index 0000000..9e4dc90
--- /dev/null
+++ b/src/export-import/CsvExportDialog.cpp
@@ -0,0 +1,137 @@
+#include "CsvExportDialog.h"
+
+#include "../strings.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicCsvWriter.h"
+
+CsvExportDialog::CsvExportDialog(QWidget* parent, const Dictionary* dict):
+ CsvDialog(parent),
+ dictionary(dict),
+ dicWriter(new DicCsvWriter(dict))
+{
+ init();
+ setWindowTitle(tr("Export to CSV"));
+
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(updateTextDelimiterCombo()) );
+ connect( showInvisibleCharsCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateCharVisibility()) );
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateQuoteAllFieldsCB()) );
+
+ connect( usedColsEdit, SIGNAL(textChanged(QString)), this, SLOT(updatePreview()) );
+ connect( quoteAllFieldsCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( writeColumnNamesCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+}
+
+CsvExportDialog::~CsvExportDialog()
+{
+ delete dicWriter;
+}
+
+QLayout* CsvExportDialog::createLeftGroupLayout()
+{
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("C&haracter set:"), createCharSetCombo());
+ lt->addRow(tr("Used &columns:"), createUsedColsEdit());
+ lt->addRow(createWriteColumnNamesBox());
+ return lt;
+}
+
+QWidget* CsvExportDialog::createUsedColsEdit()
+{
+ usedColsEdit = new QLineEdit("123");
+ usedColsEdit->setValidator(new QRegExpValidator(QRegExp("[1-9]+"), this));
+ return usedColsEdit;
+}
+
+QWidget* CsvExportDialog::createWriteColumnNamesBox()
+{
+ writeColumnNamesCB = new QCheckBox(tr("Write column &names"));
+ writeColumnNamesCB->setChecked( true );
+ return writeColumnNamesCB;
+}
+
+QLayout* CsvExportDialog::createSeparatorsLayout()
+{
+ quoteAllFieldsCB = new QCheckBox(tr("&Quote all fields"));
+
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("Field &separator:"), createFieldSeparatorWidget(SpaceChar));
+ lt->addRow(createTextDelimiterBox(), createTextDelimiterCombo());
+ lt->addRow(quoteAllFieldsCB);
+ lt->addRow(tr("Co&mment character:"), createCommentCharCombo());
+ return lt;
+}
+
+QLayout* CsvExportDialog::createPreviewLt()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addWidget(createPreview());
+ lt->addWidget(createShowInvisibleBox());
+ return lt;
+}
+
+QWidget* CsvExportDialog::createPreview()
+{
+ csvPreview = new QTextEdit;
+ csvPreview->setReadOnly(true);
+ csvPreview->setLineWrapMode( QTextEdit::NoWrap );
+ csvPreview->setFontPointSize( 10 );
+ return csvPreview;
+}
+
+QCheckBox* CsvExportDialog::createShowInvisibleBox()
+{
+ showInvisibleCharsCB = new QCheckBox(tr("Show &invisible characters"));
+ showInvisibleCharsCB->setChecked( false );
+ return showInvisibleCharsCB;
+}
+
+void CsvExportDialog::UpdateQuoteAllFieldsCB()
+{
+ quoteAllFieldsCB->setEnabled( textDelimiterCB->isChecked() );
+}
+
+void CsvExportDialog::updatePreview()
+{
+ CsvExportData params;
+ params.usedCols = getUsedColumns();
+ params.fieldSeparators = setCharVisibility( separatorsEdit->text(), false );
+ params.textDelimiter = getTextDelimiterChar();
+ params.quoteAllFields = quoteAllFieldsCB->isChecked();
+ params.commentChar = commentCharCombo->currentText()[0];
+ params.writeColumnNames = writeColumnNamesCB->isChecked();
+
+ QString csvString = dicWriter->toCsvString( params );
+ csvPreview->setPlainText(
+ setCharVisibility(csvString, showInvisibleCharsCB->isChecked()));
+}
+
+QList<int> CsvExportDialog::getUsedColumns()
+{
+ QList<int> res;
+ for(int i = 0; i < usedColsEdit->text().length(); i++)
+ {
+ int col = usedColsEdit->text()[i].digitValue();
+ if(col > 0 && col <= dictionary->fieldsNum())
+ res << col - 1;
+ }
+ return res;
+}
+
+void CsvExportDialog::SaveCSVToFile( const QString& aFilePath )
+{
+ QFile file( aFilePath );
+ if( !file.open( QIODevice::WriteOnly | QFile::Text ) ) // \r\n --> \n
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), tr("Cannot save to file:\n %1.").arg(aFilePath) );
+ return;
+ }
+ QTextStream outStream( &file );
+ outStream.setCodec( getTextCodec() );
+ outStream << setCharVisibility( csvPreview->toPlainText(), false );
+}
+
+void CsvExportDialog::UpdateCharVisibility()
+{
+ csvPreview->setPlainText(
+ setCharVisibility(csvPreview->toPlainText(), showInvisibleCharsCB->isChecked()) );
+}
diff --git a/src/export-import/CsvExportDialog.h b/src/export-import/CsvExportDialog.h
new file mode 100644
index 0000000..d99c421
--- /dev/null
+++ b/src/export-import/CsvExportDialog.h
@@ -0,0 +1,52 @@
+#ifndef CSVEXPORTDIALOG_H
+#define CSVEXPORTDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "CsvDialog.h"
+
+class Dictionary;
+class DicCsvWriter;
+
+class CsvExportDialog: public CsvDialog
+{
+ Q_OBJECT
+
+public:
+ CsvExportDialog(QWidget* parent, const Dictionary* dict );
+ ~CsvExportDialog();
+
+ void SaveCSVToFile( const QString& aFilePath );
+
+protected:
+ QLayout* createLeftGroupLayout();
+ QString getLeftGroupTitle() { return tr("Output"); }
+ QLayout* createSeparatorsLayout();
+ QLayout* createPreviewLt();
+
+protected slots:
+ void updatePreview();
+
+private slots:
+ void UpdateQuoteAllFieldsCB();
+ void UpdateCharVisibility();
+
+private:
+ QWidget* createUsedColsEdit();
+ QWidget* createWriteColumnNamesBox();
+ QList<int> getUsedColumns();
+ QWidget* createPreview();
+ QCheckBox* createShowInvisibleBox();
+
+private:
+ const Dictionary* dictionary; // not own, created here
+ DicCsvWriter* dicWriter;
+ QLineEdit* usedColsEdit;
+ QCheckBox* writeColumnNamesCB;
+ QCheckBox* quoteAllFieldsCB;
+ QTextEdit* csvPreview;
+ QCheckBox* showInvisibleCharsCB;
+};
+
+#endif
diff --git a/src/export-import/CsvImportDialog.cpp b/src/export-import/CsvImportDialog.cpp
new file mode 100644
index 0000000..51c3c87
--- /dev/null
+++ b/src/export-import/CsvImportDialog.cpp
@@ -0,0 +1,155 @@
+#include "CsvImportDialog.h"
+
+#include "../main-view/DictTableView.h"
+#include "../main-view/DictTableModel.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicCsvReader.h"
+
+CsvImportDialog::CsvImportDialog(QWidget* parent, QString filePath, const AppModel* appModel):
+ CsvDialog(parent),
+ filePath(filePath),
+ dictionary(NULL),
+ appModel(appModel)
+{
+ init();
+ setWindowTitle(tr("Import from CSV"));
+
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(updateTextDelimiterCombo()) );
+ connect( commentCharCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateCommentCharacterCombo()) );
+ connect( this, SIGNAL(rejected()), this, SLOT(DeleteDictionary()) );
+
+ connect( charSetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePreview()) );
+ connect( fromLineSpin, SIGNAL(valueChanged(int)), this, SLOT(updatePreview()) );
+ connect( firstLineIsHeaderCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( anyCharacterRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( anyCombinationRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( exactStringRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( commentCharCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( colsToImportSpin, SIGNAL(valueChanged(int)), this, SLOT(updatePreview()) );
+}
+
+CsvImportDialog::~CsvImportDialog()
+{
+ delete iPreviewModel;
+ delete dicReader;
+}
+
+QLayout* CsvImportDialog::createLeftGroupLayout()
+{
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("C&haracter set:"), createCharSetCombo());
+ lt->addRow(tr("From &line:"), createFromLineSpin());
+ lt->addRow(tr("Number of colum&ns:"), createColsToImportSpin());
+ lt->addRow(createFirstLineIsHeaderCB());
+ return lt;
+}
+
+QWidget* CsvImportDialog::createFromLineSpin()
+{
+ fromLineSpin = new QSpinBox();
+ fromLineSpin->setRange(1, 100);
+ return fromLineSpin;
+}
+
+QWidget* CsvImportDialog::createColsToImportSpin()
+{
+ colsToImportSpin = new QSpinBox();
+ colsToImportSpin->setRange(0, 9);
+ colsToImportSpin->setSpecialValueText(tr("All"));
+ return colsToImportSpin;
+}
+
+QWidget* CsvImportDialog::createFirstLineIsHeaderCB()
+{
+ firstLineIsHeaderCB = new QCheckBox(tr("&First line has field names"));
+ firstLineIsHeaderCB->setChecked(false);
+ return firstLineIsHeaderCB;
+}
+
+QLayout* CsvImportDialog::createSeparatorsLayout()
+{
+ createSeparationRadioButtons();
+
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("Field &separator:"),
+ createFieldSeparatorWidget());
+ lt->addRow(tr("Separation mode:"), anyCharacterRB);
+ // The label is a workaround to make enough space for the radio button text
+ lt->addRow(" ", anyCombinationRB);
+ lt->addRow(NULL, exactStringRB);
+ lt->addRow(createTextDelimiterBox(), createTextDelimiterCombo());
+ lt->addRow(createCommentCharBox(), createCommentCharCombo());
+ return lt;
+}
+
+void CsvImportDialog::createSeparationRadioButtons()
+{
+ anyCharacterRB = new QRadioButton(tr("An&y character"));
+ anyCharacterRB->setToolTip(tr("Fields are separated by any separator character"));
+ anyCombinationRB = new QRadioButton(tr("A co&mbination of characters"));
+ anyCombinationRB->setToolTip(tr("Fields are separated by a combination of separator characters, in any order"));
+ anyCombinationRB->setChecked( true );
+ exactStringRB = new QRadioButton(tr("E&xact string"));
+ exactStringRB->setToolTip(tr("Fields are separated by the exact string of separators, in the above defined order"));
+}
+
+QWidget* CsvImportDialog::createCommentCharBox()
+{
+ commentCharCB = new QCheckBox(tr("&Comment character:"));
+ commentCharCB->setChecked(true);
+ return commentCharCB;
+}
+
+QLayout* CsvImportDialog::createPreviewLt()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addWidget(createPreview());
+ return lt;
+}
+
+QWidget* CsvImportDialog::createPreview()
+{
+ dictionary = new Dictionary( filePath + Dictionary::DictFileExtension, true, appModel );
+ dicReader = new DicCsvReader( dictionary );
+ iPreviewModel = new DictTableModel( dictionary );
+ previewTable = new DictTableView(iPreviewModel);
+ return previewTable;
+}
+
+void CsvImportDialog::UpdateCommentCharacterCombo()
+{
+ commentCharCombo->setEnabled( commentCharCB->isChecked() );
+}
+
+void CsvImportDialog::updatePreview()
+{
+ CsvImportData params;
+ params.textCodec = getTextCodec();
+ params.fromLine = fromLineSpin->value();
+ params.fieldSeparators = setCharVisibility(separatorsEdit->text(), false);
+ params.fieldSeparationMode = getSeparationMode();
+ params.commentChar = commentCharCB->isChecked()?
+ commentCharCombo->currentText()[0]: QChar(0);
+ params.textDelimiter = getTextDelimiterChar();
+ params.colsToImport = colsToImportSpin->value();
+ params.firstLineIsHeader = firstLineIsHeaderCB->isChecked();
+
+ dicReader->readDict( filePath, params );
+ iPreviewModel->resetData();
+}
+
+FieldSeparationMode CsvImportDialog::getSeparationMode()
+{
+ if(anyCharacterRB->isChecked())
+ return EFieldSeparatorAnyCharacter;
+ else if (anyCombinationRB->isChecked())
+ return EFieldSeparatorAnyCombination;
+ else
+ return EFieldSeparatorExactString;
+}
+
+void CsvImportDialog::DeleteDictionary()
+{
+delete dictionary;
+dictionary = NULL;
+}
diff --git a/src/export-import/CsvImportDialog.h b/src/export-import/CsvImportDialog.h
new file mode 100644
index 0000000..bc3a9d1
--- /dev/null
+++ b/src/export-import/CsvImportDialog.h
@@ -0,0 +1,64 @@
+#ifndef CSVIMPORTDIALOG_H
+#define CSVIMPORTDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "CsvData.h"
+#include "CsvDialog.h"
+
+class DictTableView;
+class Dictionary;
+class DicCsvReader;
+class DictTableModel;
+class AppModel;
+
+class CsvImportDialog: public CsvDialog
+{
+ Q_OBJECT
+
+public:
+ CsvImportDialog(QWidget* parent, QString filePath, const AppModel* appModel );
+ ~CsvImportDialog();
+
+ Dictionary* getDictionary() { return dictionary; }
+
+protected:
+ QLayout* createLeftGroupLayout();
+ QString getLeftGroupTitle() { return tr("Input"); }
+ QLayout* createSeparatorsLayout();
+ QLayout* createPreviewLt();
+
+protected slots:
+ void updatePreview();
+
+private slots:
+ void UpdateCommentCharacterCombo();
+ void DeleteDictionary();
+
+private:
+ QWidget* createFromLineSpin();
+ QWidget* createColsToImportSpin();
+ QWidget* createFirstLineIsHeaderCB();
+ void createSeparationRadioButtons();
+ FieldSeparationMode getSeparationMode();
+ QWidget* createCommentCharBox();
+ QWidget* createPreview();
+
+private:
+ QString filePath;
+ Dictionary* dictionary; // not own, created here
+ const AppModel* appModel;
+ DicCsvReader* dicReader;
+ DictTableModel* iPreviewModel;
+ QSpinBox* fromLineSpin; // 1-based
+ QSpinBox* colsToImportSpin; // 1-based; 0 = all
+ QCheckBox* firstLineIsHeaderCB;
+ QRadioButton* anyCharacterRB;
+ QRadioButton* anyCombinationRB;
+ QRadioButton* exactStringRB;
+ QCheckBox* commentCharCB;
+ DictTableView* previewTable;
+};
+
+#endif
diff --git a/src/field-styles/FieldStyle.cpp b/src/field-styles/FieldStyle.cpp
new file mode 100644
index 0000000..4e710f8
--- /dev/null
+++ b/src/field-styles/FieldStyle.cpp
@@ -0,0 +1,38 @@
+#include "FieldStyle.h"
+
+FieldStyle::FieldStyle():
+ FieldStyle("", 0) {}
+
+FieldStyle::FieldStyle(const QString& family, int size,
+ bool bold, bool italic, const QString& colorName,
+ const QString& prefix, const QString& suffix,
+ bool hasKeyword, const QString& keywordColorName):
+ color(Qt::black),
+ prefix(prefix), suffix(suffix),
+ hasKeyword(hasKeyword),
+ keywordBold(false),
+ keywordItalic(false)
+{
+ font.setFamily("Times New Roman");
+ font.setPointSize(18);
+ if(!family.isEmpty())
+ font.setFamily(family);
+ if(size > 0)
+ font.setPointSize(size);
+ font.setBold(bold);
+ font.setItalic(italic);
+ if(!colorName.isEmpty())
+ color.setNamedColor(colorName);
+ keywordColor.setNamedColor(keywordColorName);
+}
+
+FieldStyle FieldStyle::getKeywordStyle() const
+{
+ if(!hasKeyword)
+ return *this;
+ FieldStyle keywordStyle = *this;
+ keywordStyle.font.setBold(keywordBold);
+ keywordStyle.font.setItalic(keywordItalic);
+ keywordStyle.color = keywordColor;
+ return keywordStyle;
+}
diff --git a/src/field-styles/FieldStyle.h b/src/field-styles/FieldStyle.h
new file mode 100644
index 0000000..20da858
--- /dev/null
+++ b/src/field-styles/FieldStyle.h
@@ -0,0 +1,28 @@
+#ifndef FIELDSTYLE_H
+#define FIELDSTYLE_H
+
+#include <QFont>
+#include <QColor>
+
+class FieldStyle
+{
+public:
+ FieldStyle();
+ FieldStyle(const QString& family, int size,
+ bool bold = true, bool italic = false, const QString& colorName = "",
+ const QString& prefix = "", const QString& suffix = "",
+ bool hasKeyword = false, const QString& keywordColorName = "#000000");
+ FieldStyle getKeywordStyle() const;
+
+public:
+ QFont font;
+ QColor color;
+ QString prefix;
+ QString suffix;
+ bool hasKeyword;
+ bool keywordBold;
+ bool keywordItalic;
+ QColor keywordColor;
+};
+
+#endif
diff --git a/src/field-styles/FieldStyleFactory.cpp b/src/field-styles/FieldStyleFactory.cpp
new file mode 100644
index 0000000..ab789e3
--- /dev/null
+++ b/src/field-styles/FieldStyleFactory.cpp
@@ -0,0 +1,145 @@
+#include "FieldStyleFactory.h"
+
+const QString FieldStyleFactory::DefaultStyle = "Normal";
+
+FieldStyleFactory::FieldStyleFactory()
+ {
+ cardBgColor.setNamedColor("white");
+ initStyles();
+ }
+
+void FieldStyleFactory::initStyles()
+{
+ setStyle(DefaultStyle, FieldStyle());
+ setStyle("Example", {"", 14, false, false, "", "", "", true, "blue"});
+ setStyle("Transcription", {"Arial", 0, false, false, "", "/", "/"});
+ setStyle("Big", {"Arial", 26});
+ setStyle("Color1", {"", 0, true, false, "red"});
+ setStyle("Color2", {"", 0, true, false, "blue"});
+}
+
+FieldStyleFactory* FieldStyleFactory::inst()
+ {
+ static FieldStyleFactory factory;
+ return &factory;
+ }
+
+void FieldStyleFactory::setStyle(const QString& aStyleName, FieldStyle aStyle )
+ {
+ styles[aStyleName] = aStyle;
+ if(!stylesNames.contains( aStyleName ))
+ stylesNames << aStyleName;
+ }
+
+void FieldStyleFactory::load()
+{
+ QSettings settings;
+ settings.beginGroup("Styles");
+ cardBgColor.setNamedColor(
+ settings.value("bg-color", cardBgColor.name()).toString());
+ foreach(QString styleName,
+ settings.value("list", getStyleNames()).toStringList())
+ loadStyle(settings, styleName);
+ settings.endGroup();
+}
+
+void FieldStyleFactory::loadStyle(QSettings& settings, const QString& styleName)
+{
+ FieldStyle curStyle;
+ settings.beginGroup(styleName);
+ FieldStyle defaultStyle = getStyle(styleName);
+ loadStyleMainProperties(settings, curStyle, defaultStyle);
+ loadKeywordStyle(settings, curStyle, defaultStyle);
+ settings.endGroup();
+ setStyle(styleName, curStyle);
+}
+
+void FieldStyleFactory::loadStyleMainProperties(const QSettings& settings,
+ FieldStyle& curStyle, const FieldStyle& defaultStyle)
+{
+ curStyle.font.setFamily(
+ settings.value("font-family", defaultStyle.font.family()).toString());
+ curStyle.font.setPointSize(
+ settings.value("font-size", defaultStyle.font.pointSize()).toInt());
+ curStyle.font.setBold(
+ settings.value("font-bold", defaultStyle.font.bold()).toBool());
+ curStyle.font.setItalic(
+ settings.value("font-italic", defaultStyle.font.italic()).toBool());
+ curStyle.color.setNamedColor(
+ settings.value("color", defaultStyle.color.name()).toString());
+ curStyle.prefix = settings.value("prefix", defaultStyle.prefix).toString();
+ curStyle.suffix = settings.value("suffix", defaultStyle.suffix).toString();
+ curStyle.hasKeyword = settings.value("keyword", defaultStyle.hasKeyword).toBool();
+}
+
+void FieldStyleFactory::loadKeywordStyle(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(!curStyle.hasKeyword)
+ return;
+ curStyle.keywordBold =
+ settings.value("keyword/font-bold", defaultStyle.keywordBold).toBool();
+ curStyle.keywordItalic =
+ settings.value("keyword/font-italic", defaultStyle.keywordItalic).toBool();
+ curStyle.keywordColor.setNamedColor(
+ settings.value("keyword/color", defaultStyle.keywordColor.name()).toString());
+}
+
+void FieldStyleFactory::save()
+ {
+ QSettings settings;
+ settings.beginGroup("Styles");
+ settings.remove(""); // Remove old user settings
+ if(cardBgColor != FieldStyleFactory().cardBgColor)
+ settings.setValue("bg-color", cardBgColor.name());
+ foreach(QString styleName, getStyleNames())
+ saveStyle(settings, styleName);
+ settings.endGroup();
+ }
+
+void FieldStyleFactory::saveStyle(QSettings& settings, const QString& styleName)
+{
+ const FieldStyle curStyle = getStyle(styleName);
+ const FieldStyle defaultStyle = FieldStyleFactory().getStyle(styleName);
+ settings.beginGroup( styleName );
+ saveStyleMainProperties(settings, curStyle, defaultStyle);
+ saveKeywordStyle(settings, curStyle, defaultStyle);
+ settings.endGroup();
+}
+
+void FieldStyleFactory::saveStyleMainProperties(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(curStyle.font.family() != defaultStyle.font.family())
+ settings.setValue("font-family", curStyle.font.family());
+ if(curStyle.font.pointSize() != defaultStyle.font.pointSize())
+ settings.setValue("font-size", curStyle.font.pointSize());
+ if(curStyle.font.bold() != defaultStyle.font.bold())
+ settings.setValue("font-bold", curStyle.font.bold());
+ if(curStyle.font.italic() != defaultStyle.font.italic())
+ settings.setValue("font-italic", curStyle.font.italic());
+ if(curStyle.color != defaultStyle.color)
+ settings.setValue("color", curStyle.color.name());
+ if(curStyle.prefix != defaultStyle.prefix)
+ settings.setValue("prefix", curStyle.prefix);
+ if(curStyle.suffix != defaultStyle.suffix)
+ settings.setValue("suffix", curStyle.suffix);
+ if(curStyle.hasKeyword != defaultStyle.hasKeyword)
+ settings.setValue("keyword", curStyle.hasKeyword);
+}
+
+void FieldStyleFactory::saveKeywordStyle(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(!curStyle.hasKeyword)
+ return;
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordBold != defaultStyle.keywordBold)
+ settings.setValue("keyword/font-bold", curStyle.keywordBold);
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordItalic != defaultStyle.keywordItalic)
+ settings.setValue("keyword/font-italic", curStyle.keywordItalic);
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordColor != defaultStyle.keywordColor)
+ settings.setValue("keyword/color", curStyle.keywordColor.name());
+}
diff --git a/src/field-styles/FieldStyleFactory.h b/src/field-styles/FieldStyleFactory.h
new file mode 100644
index 0000000..072bdd5
--- /dev/null
+++ b/src/field-styles/FieldStyleFactory.h
@@ -0,0 +1,49 @@
+#ifndef FIELDSTYLEFACTORY_H
+#define FIELDSTYLEFACTORY_H
+
+#include "FieldStyle.h"
+
+#include <QtCore>
+
+class FieldStyleFactory
+{
+public:
+ static FieldStyleFactory* inst();
+
+public:
+ FieldStyleFactory();
+ const QStringList getStyleNames() const
+ { return stylesNames; }
+ const FieldStyle getStyle(const QString& aStyleName) const
+ { return styles[aStyleName]; }
+ FieldStyle* getStylePtr( const QString& aStyleName )
+ { return &( styles[aStyleName] ); }
+ void setStyle(const QString& aStyleName, FieldStyle aStyle );
+ void load();
+ void save();
+
+private:
+ void initStyles();
+ void loadStyle(QSettings& settings, const QString& styleName);
+ void loadStyleMainProperties(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void loadKeywordStyle(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void saveStyle(QSettings& settings, const QString& styleName);
+ void saveStyleMainProperties(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void saveKeywordStyle(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+
+public:
+ static const QString DefaultStyle;
+
+public:
+ QColor cardBgColor;
+
+private:
+ QHash<QString, FieldStyle> styles;
+ QStringList stylesNames; // Keeps order of styles
+};
+
+#endif
diff --git a/src/main-view/AboutDialog.cpp b/src/main-view/AboutDialog.cpp
new file mode 100644
index 0000000..f1a7839
--- /dev/null
+++ b/src/main-view/AboutDialog.cpp
@@ -0,0 +1,27 @@
+#include "AboutDialog.h"
+#include "../strings.h"
+#include "../version.h"
+
+AboutDialog::AboutDialog(QWidget* parent):
+ QMessageBox(parent)
+{
+ setIconPixmap(QPixmap(":/images/freshmemory.png"));
+ setWindowTitle(tr("About %1").arg( Strings::tr(Strings::s_appTitle)));
+ setText(createAboutText());
+ setEscapeButton(addButton(QMessageBox::Ok));
+}
+
+QString AboutDialog::createAboutText()
+{
+ QString formattedBuildStr;
+ if( !BuildStr.isEmpty() )
+ formattedBuildStr = "<p style=\"font-size:10pt\">" + BuildStr + "</p>";
+ return QString("<p><h2>") + Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "</h2></p>" +
+ formattedBuildStr +
+ "<p>" + tr("Learn new things quickly and keep your memory fresh with time spaced repetition.") + "</p>" +
+ "<p>" + Strings::tr(Strings::s_author) + "</p>" +
+ "<p><a href=\"http://fresh-memory.com\"> fresh-memory.com </a></p>" +
+ "</a></p>" +
+ "<p>" + tr("License:") + " <a href=\"http://www.gnu.org/copyleft/gpl.html\"> GPL 3" +
+ "</a></p>" + "<p><img src=\":/images/gplv3-88x31.png\"></p>";
+ }
diff --git a/src/main-view/AboutDialog.h b/src/main-view/AboutDialog.h
new file mode 100644
index 0000000..d05c11c
--- /dev/null
+++ b/src/main-view/AboutDialog.h
@@ -0,0 +1,17 @@
+#ifndef ABOUT_DIALOG_H
+#define ABOUT_DIALOG_H
+
+#include <QtWidgets>
+
+class AboutDialog: public QMessageBox
+{
+ Q_OBJECT
+
+public:
+ AboutDialog(QWidget* parent);
+
+private:
+ QString createAboutText();
+};
+
+#endif
diff --git a/src/main-view/AppModel.cpp b/src/main-view/AppModel.cpp
new file mode 100644
index 0000000..45e1956
--- /dev/null
+++ b/src/main-view/AppModel.cpp
@@ -0,0 +1,196 @@
+#include "AppModel.h"
+
+#include <stdlib.h>
+
+#include "../utils/RandomGenerator.h"
+#include "../dictionary/Dictionary.h"
+#include "../main-view/DictTableModel.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../dictionary/CardPack.h"
+
+AppModel::AppModel():
+ curDictIndex(-1), curCardPackIndex(0)
+{
+}
+
+AppModel::~AppModel()
+{
+ while(!dictionaries.isEmpty())
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeLast();
+ delete pair.second;
+ delete pair.first;
+ }
+}
+
+bool AppModel::openDictionary(const QString& filePath)
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ bool ok = dict->load(filePath);
+ if(ok)
+ addDictionary(dict);
+ else
+ {
+ errorMessage = dict->getErrorMessage();
+ delete dict;
+ }
+ return ok;
+}
+
+void AppModel::addDictionary( Dictionary* aDict )
+{
+ DictTableModel* dictModel = new DictTableModel( aDict );
+ dictionaries << qMakePair( aDict, dictModel );
+ curDictIndex = dictionaries.size() - 1;
+}
+
+Dictionary* AppModel::newDictionary()
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ dict->setDefaultFields();
+ addDictionary( dict );
+ return dict;
+}
+
+void AppModel::fixupCurDictIx()
+{
+ if( curDictIndex < 0 || curDictIndex >= dictionaries.size() )
+ curDictIndex = dictionaries.size()-1;
+}
+
+Dictionary* AppModel::curDictionary()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].first;
+ }
+
+DictTableModel* AppModel::curDictModel()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].second;
+ }
+
+Dictionary* AppModel::dictionary(int aIndex) const
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return NULL;
+ return dictionaries[aIndex].first;
+ }
+
+int AppModel::indexOfDictionary( Dictionary* aDic ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.first == aDic )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+int AppModel::indexOfDictionary( const QString& aFilePath ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ Q_ASSERT( pair.first );
+ if( pair.first->getFilePath() == aFilePath )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+CardPack* AppModel::curCardPack()
+ {
+ Dictionary* curDic = curDictionary();
+ if( curDic )
+ return curDic->cardPack( curCardPackIndex );
+ else
+ return NULL;
+ }
+
+bool AppModel::setCurDictionary(int index)
+ {
+ if( dictionaries.isEmpty() )
+ {
+ curDictIndex = -1;
+ return false;
+ }
+ if( index < 0 || index >= dictionaries.size())
+ {
+ curDictIndex = dictionaries.size()-1;
+ return false;
+ }
+ curDictIndex = index;
+ return true;
+ }
+
+bool AppModel::removeDictionary( int aIndex )
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return false;
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeAt( aIndex );
+ delete pair.second;
+ delete pair.first;
+ return true;
+ }
+
+/** Destroys dic model and the dictionary itself.
+ */
+void AppModel::removeDictModel( QAbstractItemModel* aDictModel )
+ {
+ int i = 0;
+ for( ;i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.second == aDictModel )
+ {
+ dictionaries.removeAt( i ); // Update the size of m_dictionaries before destroying
+ delete pair.first;
+ delete pair.second;
+ break;
+ }
+ }
+ }
+
+IStudyModel* AppModel::createStudyModel(int studyType, int cardPackIndex)
+ {
+ Dictionary* curDic = curDictionary();
+ if(!curDic)
+ {
+ errorMessage = tr("No dictionary opened.");
+ return NULL;
+ }
+ if(curDic->entriesNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ curCardPackIndex = cardPackIndex;
+ CardPack* cardPack = curDic->cardPack(curCardPackIndex);
+ if(!cardPack || cardPack->cardsNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ IStudyModel* studyModel = NULL;
+ switch(studyType)
+ {
+ case WordDrill:
+ studyModel = new WordDrillModel(cardPack);
+ break;
+ case SpacedRepetition:
+ studyModel = new SpacedRepetitionModel(cardPack, new RandomGenerator);
+ break;
+ }
+ if(studyModel)
+ studyModel->setDictModel( curDictModel() );
+ return studyModel;
+ }
diff --git a/src/main-view/AppModel.h b/src/main-view/AppModel.h
new file mode 100644
index 0000000..bf1ddd2
--- /dev/null
+++ b/src/main-view/AppModel.h
@@ -0,0 +1,64 @@
+#ifndef APPMODEL_H
+#define APPMODEL_H
+
+#include <QList>
+#include <QPair>
+#include <QFile>
+#include <QAbstractItemModel>
+
+class Dictionary;
+class DictTableModel;
+class CardPack;
+class IStudyModel;
+
+class AppModel: public QObject
+{
+Q_OBJECT
+
+public:
+ enum StudyType
+ {
+ WordDrill,
+ SpacedRepetition,
+ StudyTypesNum
+ };
+
+public:
+ AppModel();
+ ~AppModel();
+
+ bool openDictionary( const QString& filePath );
+ void addDictionary( Dictionary* aDict );
+ Dictionary* newDictionary();
+ Dictionary* curDictionary();
+ int getCurDictIndex() const { return curDictIndex; }
+ DictTableModel* curDictModel();
+ int dictionariesNum() const { return dictionaries.size(); }
+ Dictionary* dictionary(int aIndex) const;
+ int indexOfDictionary( Dictionary* aDic ) const;
+ int indexOfDictionary( const QString& aFilePath ) const;
+ // TODO: Synchronize this with the pack tree view selection
+ int curCardPackIx() const { return curCardPackIndex; }
+ CardPack* curCardPack();
+ QString getErrorMessage() const { return errorMessage; }
+
+public slots:
+ /** @return true, if the dictionary was successfully set. false, if there are no dictionaries, or the index is out of range.
+ * If the index is out of range, the last dictionary from the list is set.
+ */
+ bool setCurDictionary(int index);
+ bool removeDictionary(int aIndex);
+ void removeDictModel(QAbstractItemModel* aDictModel);
+ IStudyModel* createStudyModel(int studyType, int cardPackIndex);
+
+private:
+ void fixupCurDictIx();
+
+private:
+ QList< QPair<Dictionary*, DictTableModel*> > dictionaries;
+ QString errorMessage;
+ int curDictIndex;
+ int curCardPackIndex;
+};
+
+#endif
diff --git a/src/main-view/CardFilterModel.cpp b/src/main-view/CardFilterModel.cpp
new file mode 100644
index 0000000..50ac75d
--- /dev/null
+++ b/src/main-view/CardFilterModel.cpp
@@ -0,0 +1,24 @@
+#include "CardFilterModel.h"
+
+CardFilterModel::CardFilterModel( QObject* parent ):
+ QSortFilterProxyModel( parent )
+ {
+ }
+
+void CardFilterModel::addFilterRow( int aRow )
+ {
+ if( m_filterRows.contains( aRow ) )
+ return;
+ m_filterRows << aRow;
+ }
+
+void CardFilterModel::removeFilterRow( int aRow )
+ {
+ m_filterRows.removeOne( aRow );
+ }
+
+bool CardFilterModel::filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const
+ {
+ Q_UNUSED( source_parent );
+ return m_filterRows.contains( source_row );
+ }
diff --git a/src/main-view/CardFilterModel.h b/src/main-view/CardFilterModel.h
new file mode 100644
index 0000000..7c6e9f2
--- /dev/null
+++ b/src/main-view/CardFilterModel.h
@@ -0,0 +1,21 @@
+#ifndef CARDFILTERMODEL_H
+#define CARDFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+class CardFilterModel : public QSortFilterProxyModel
+ {
+ Q_OBJECT
+public:
+ CardFilterModel( QObject* parent );
+ void addFilterRow( int aRow );
+ void removeFilterRow( int aRow );
+
+protected:
+ bool filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const;
+
+private:
+ QList<int> m_filterRows;
+ };
+
+#endif // CARDFILTERMODEL_H
diff --git a/src/main-view/CardPreview.cpp b/src/main-view/CardPreview.cpp
new file mode 100644
index 0000000..aa59031
--- /dev/null
+++ b/src/main-view/CardPreview.cpp
@@ -0,0 +1,55 @@
+#include "CardPreview.h"
+#include "../study/CardSideView.h"
+#include "../dictionary/Card.h"
+
+CardPreview::CardPreview(QWidget* parent):
+ QDockWidget(parent)
+{
+ setDockProperties();
+ createCardSides();
+ createInternalWidget();
+}
+
+void CardPreview::setDockProperties()
+{
+ setWindowTitle(tr("Card preview"));
+ setObjectName("Card-preview");
+ setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+}
+
+void CardPreview::createCardSides()
+{
+ questionSide = new CardSideView(CardSideView::QstMode);
+ answerSide = new CardSideView(CardSideView::AnsMode);
+}
+
+void CardPreview::createInternalWidget()
+{
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(questionSide, 1);
+ mainLt->addWidget(answerSide, 1);
+
+ QWidget* internalWidget = new QWidget;
+ internalWidget->setLayout(mainLt);
+ setWidget(internalWidget);
+}
+
+void CardPreview::setPack(const CardPack* pack)
+{
+ questionSide->setPack(pack);
+ answerSide->setPack(pack);
+}
+
+void CardPreview::setContent(const CardPack* pack, Card* card)
+{
+ setPack(pack);
+ QString question;
+ QStringList answers;
+ if(card)
+ {
+ question = card->getQuestion();
+ answers = card->getAnswers();
+ }
+ questionSide->setQuestion(question);
+ answerSide->setQstAnsr(question, answers);
+}
diff --git a/src/main-view/CardPreview.h b/src/main-view/CardPreview.h
new file mode 100644
index 0000000..6217c96
--- /dev/null
+++ b/src/main-view/CardPreview.h
@@ -0,0 +1,30 @@
+#ifndef CARD_PREVIEW_H
+#define CARD_PREVIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class CardSideView;
+class Card;
+class CardPack;
+
+class CardPreview: public QDockWidget
+{
+ Q_OBJECT
+
+public:
+ CardPreview(QWidget* parent);
+ void setContent(const CardPack* pack, Card* card);
+
+private:
+ void createCardSides();
+ void createInternalWidget();
+ void setDockProperties();
+ void setPack(const CardPack* pack);
+
+private:
+ CardSideView* questionSide;
+ CardSideView* answerSide;
+};
+
+#endif
diff --git a/src/main-view/DictTableDelegate.cpp b/src/main-view/DictTableDelegate.cpp
new file mode 100644
index 0000000..1e1d609
--- /dev/null
+++ b/src/main-view/DictTableDelegate.cpp
@@ -0,0 +1,128 @@
+#include "DictTableDelegate.h"
+#include <QtDebug>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "DictTableDelegatePainter.h"
+#include "FieldContentCodec.h"
+#include "../dictionary/Dictionary.h"
+
+QWidget* DictTableDelegate::createEditor( QWidget* parent,
+ const QStyleOptionViewItem& option, const QModelIndex& /*index*/ ) const
+{
+ editor = new RecordEditor(parent, option.rect);
+ connect( editor, SIGNAL(destroyed()), SIGNAL(editorDestroyed()) );
+ emit editorCreated();
+ return editor;
+}
+
+void DictTableDelegate::updateEditorGeometry(QWidget* editor,
+ const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const
+{
+ setEditorData(editor, index);
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ recordEditor->updateEditor();
+}
+
+bool DictTableDelegate::eventFilter(QObject *object, QEvent *event)
+{
+ QWidget* editor = qobject_cast<QWidget*>(object);
+ if (!editor)
+ return false;
+ if(event->type() == QEvent::KeyPress)
+ switch( static_cast<QKeyEvent*>(event)->key() )
+ {
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ case Qt::Key_Tab:
+ emit commitData(editor);
+ emit closeEditor(editor, QAbstractItemDelegate::EditNextItem);
+ return true;
+ default:
+ break;
+ }
+ return QStyledItemDelegate::eventFilter(object, event);
+}
+
+void DictTableDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ FieldContentCodec codec(recordEditor);
+ codec.parse(getDisplayText(index));
+}
+
+QString DictTableDelegate::getDisplayText(const QModelIndex& index) const
+{
+ return index.data(Qt::EditRole).toString();
+}
+
+void DictTableDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const
+ {
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model );
+ QString editorText = recordEditor->getText();
+ QModelIndex origIndex = index;
+ if( !tableModel )
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model );
+ if( !proxyModel )
+ return;
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( !tableModel )
+ return;
+ origIndex = proxyModel->mapToSource( index );
+ }
+ if(editorText == getDisplayText(index) && !indexIsLastCell(origIndex, model))
+ return;
+ QString newText = tableModel->dictionary()->shortenImagePaths(editorText);
+ QUndoCommand* command = new EditRecordCmd( tableModel, origIndex, newText );
+ tableModel->undoStack()->push( command );
+}
+
+bool DictTableDelegate::indexIsLastCell(const QModelIndex& index,
+ QAbstractItemModel* model) const
+{
+ return index.row() == model->rowCount() - 1 &&
+ index.column() == model->columnCount() - 1;
+}
+
+void DictTableDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+{
+ QStyledItemDelegate::paint(painter, option, index);
+ DictTableDelegatePainter dPainter(painter, getMarginRect(option),
+ option.fontMetrics);
+ FieldContentCodec codec(&dPainter);
+ codec.parse(getDisplayText(index));
+}
+
+QRect DictTableDelegate::getMarginRect(const QStyleOptionViewItem& option) const
+{
+ const int margin = 2;
+ return option.rect.adjusted(margin, 0, -margin, 0);
+}
+
+int DictTableDelegate::getCursorPos() const
+{
+ if(editor)
+ return editor->textCursor().position();
+ else
+ return -1;
+}
+
+void DictTableDelegate::setCursorPos(int pos)
+{
+ if(!editor)
+ return;
+ QTextCursor cursor = editor->textCursor();
+ cursor.setPosition(pos);
+ editor->setTextCursor(cursor);
+}
+
+void DictTableDelegate::insertImageIntoEditor(int cursorPos, const QString& filePath)
+{
+ if(!editor)
+ return;
+ editor->insertImage(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableDelegate.h b/src/main-view/DictTableDelegate.h
new file mode 100644
index 0000000..cf43fc5
--- /dev/null
+++ b/src/main-view/DictTableDelegate.h
@@ -0,0 +1,44 @@
+#ifndef DICT_TABLE_DELEGATE_H
+#define DICT_TABLE_DELEGATE_H
+
+#include <QtWidgets>
+
+#include "RecordEditor.h"
+
+class DictTableDelegate: public QStyledItemDelegate
+{
+Q_OBJECT
+
+public:
+ DictTableDelegate(QObject* parent):
+ QStyledItemDelegate(parent), editor(NULL) {}
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ bool eventFilter(QObject *object, QEvent *event);
+ void setEditorData(QWidget* editor, const QModelIndex& index) const;
+ void setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const;
+ void paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ int getCursorPos() const;
+ void setCursorPos(int pos);
+ void insertImageIntoEditor(int cursorPos, const QString& filePath);
+ QWidget* getEditor() const { return editor; }
+
+private:
+ QString getDisplayText(const QModelIndex& index) const;
+ QRect getMarginRect(const QStyleOptionViewItem& option) const;
+ bool indexIsLastCell(const QModelIndex& index, QAbstractItemModel* model) const;
+
+signals:
+ void editorCreated() const;
+ void editorDestroyed() const;
+
+private:
+ mutable RecordEditor* editor;
+};
+
+#endif
+
diff --git a/src/main-view/DictTableDelegatePainter.cpp b/src/main-view/DictTableDelegatePainter.cpp
new file mode 100644
index 0000000..2714bdc
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.cpp
@@ -0,0 +1,61 @@
+#include "DictTableDelegatePainter.h"
+
+DictTableDelegatePainter::DictTableDelegatePainter(QPainter* painter,
+ const QRect& contentRect, const QFontMetrics& fontMetrics):
+ painter(painter), contentRect(contentRect), fontMetrics(fontMetrics)
+{
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::initLoopParams()
+{
+ offset = 0;
+ textFitstoRect = true;
+}
+
+void DictTableDelegatePainter::startDrawing()
+{
+ painter->save();
+ painter->setClipRect(contentRect);
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::endDrawing()
+{
+ painter->restore();
+}
+
+void DictTableDelegatePainter::drawText(const QString& text)
+{
+ if(!textFitstoRect)
+ return;
+ QString elidedText = getElidedText(text);
+ textFitstoRect = elidedText == text;
+ painter->drawText(contentRect.translated(offset, 0),
+ elidedText, getTextOption());
+ offset += fontMetrics.width(elidedText);
+}
+
+void DictTableDelegatePainter::drawImage(const QString& filePath)
+{
+ if(!textFitstoRect)
+ return;
+ QPixmap image(filePath);
+ if(image.isNull())
+ image = QPixmap(":/images/broken-image.png");
+ image = image.scaledToHeight(ThumbnailSize, Qt::SmoothTransformation);
+ painter->drawPixmap(contentRect.topLeft() + QPoint(offset, 0), image);
+ offset += image.width();
+}
+
+QString DictTableDelegatePainter::getElidedText(const QString& text)
+{
+ return fontMetrics.elidedText(text, Qt::ElideRight, contentRect.width() - offset);
+}
+
+QTextOption DictTableDelegatePainter::getTextOption()
+{
+ QTextOption textOption(Qt::AlignVCenter);
+ textOption.setWrapMode(QTextOption::NoWrap);
+ return textOption;
+}
diff --git a/src/main-view/DictTableDelegatePainter.h b/src/main-view/DictTableDelegatePainter.h
new file mode 100644
index 0000000..985ecac
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.h
@@ -0,0 +1,36 @@
+#ifndef DICT_TABLE_DELEGATE_PAINTER_H
+#define DICT_TABLE_DELEGATE_PAINTER_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class DictTableDelegatePainter: public FieldContentPainter
+{
+private:
+ static QTextOption getTextOption();
+
+private:
+ static const int ThumbnailSize = 25;
+
+public:
+ DictTableDelegatePainter(QPainter* painter, const QRect& contentRect,
+ const QFontMetrics& fontMetrics);
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+
+private:
+ void initLoopParams();
+ QString getElidedText(const QString& text);
+
+private:
+ QPainter* painter;
+ QRect contentRect;
+ QFontMetrics fontMetrics;
+ int offset;
+ bool textFitstoRect;
+};
+#endif
+
diff --git a/src/main-view/DictTableModel.cpp b/src/main-view/DictTableModel.cpp
new file mode 100644
index 0000000..b7b7eed
--- /dev/null
+++ b/src/main-view/DictTableModel.cpp
@@ -0,0 +1,142 @@
+#include <QtGui>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/Field.h"
+
+DictTableModel::DictTableModel( Dictionary* aDict, QObject *parent ):
+ QAbstractTableModel( parent ), m_dictionary( aDict )
+ {
+ m_undoStack = new QUndoStack( this );
+ connect( m_dictionary, SIGNAL(destroyed(QObject*)), SLOT(discardCurDictionary()) );
+ }
+
+int DictTableModel::rowCount( const QModelIndex &/*parent*/ ) const
+ {
+ if( m_dictionary )
+ return m_dictionary->entriesNum();
+ else
+ return 0;
+ }
+
+int DictTableModel::columnCount(const QModelIndex &/*parent*/) const
+ {
+ if( m_dictionary )
+ return m_dictionary->fieldsNum();
+ else
+ return 0;
+ }
+
+QVariant DictTableModel::data( const QModelIndex &index, int role ) const
+ {
+ if( !m_dictionary || !index.isValid() || index.row() >= rowCount() ||
+ index.column() >= columnCount() )
+ return QVariant();
+ QString text = m_dictionary->getFieldValue(index.row(), index.column());
+ switch(role)
+ {
+ case Qt::DisplayRole:
+ return QVariant();
+ case Qt::EditRole:
+ return m_dictionary->extendImagePaths(text);
+ case DicRecordRole:
+ {
+ const DicRecord* record = m_dictionary->getRecord(index.row());
+ return QVariant::fromValue(*record);
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+QVariant DictTableModel::headerData( int section, Qt::Orientation orientation, int role ) const
+ {
+ if( !m_dictionary )
+ return QVariant();
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ if(orientation == Qt::Vertical)
+ return QString::number( section + 1 );
+ else
+ {
+ if( section < columnCount() )
+ return m_dictionary->field( section )->name();
+ else
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+bool DictTableModel::setData( const QModelIndex &index, const QVariant &value, int role )
+ {
+ if( !index.isValid() || !m_dictionary )
+ return false;
+ if( value == data( index, role ) )
+ return false;
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ m_dictionary->setFieldValue(index.row(), index.column(), value.toString());
+ break;
+ case DicRecordRole:
+ {
+ DicRecord record = value.value<DicRecord>();
+ m_dictionary->setRecord( index.row(), record );
+ break;
+ }
+ default:
+ return false;
+ }
+ emit dataChanged( index, index );
+ return true;
+ }
+
+bool DictTableModel::insertRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->insertEntries( position, rows );
+ endInsertRows();
+ return true;
+ }
+
+bool DictTableModel::addFields( QStringList aFields )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertColumns( QModelIndex(), columnCount(), columnCount() + aFields.size() - 1 );
+ m_dictionary->addFields( aFields );
+ endInsertColumns();
+ return true;
+ }
+
+bool DictTableModel::removeRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginRemoveRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->removeRecords( position, rows );
+ endRemoveRows();
+ return true;
+ }
+
+void DictTableModel::resetData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+void DictTableModel::discardCurDictionary()
+ {
+ beginResetModel();
+ m_dictionary = NULL;
+ endResetModel();
+ }
diff --git a/src/main-view/DictTableModel.h b/src/main-view/DictTableModel.h
new file mode 100644
index 0000000..0293ace
--- /dev/null
+++ b/src/main-view/DictTableModel.h
@@ -0,0 +1,52 @@
+#ifndef DICTTABLEMODEL_H
+#define DICTTABLEMODEL_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class Dictionary;
+class DicRecord;
+
+class DictTableModel: public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum
+ {
+ DicRecordRole = Qt::UserRole
+ };
+
+ DictTableModel( Dictionary* aDict, QObject *parent = 0 );
+
+ // Getters
+
+ const Dictionary* dictionary() const { return m_dictionary; }
+ QUndoStack* undoStack() const { return m_undoStack; }
+
+ Qt::ItemFlags flags( const QModelIndex& index ) const { return QAbstractItemModel::flags( index ) | Qt::ItemIsEditable; }
+ int rowCount( const QModelIndex& parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex& parent = QModelIndex() ) const;
+ QVariant data(const QModelIndex& index, int role) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+
+ // Setters
+
+ bool setData( const QModelIndex &index, const QVariant& value, int role = Qt::EditRole );
+ bool insertRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+ bool addFields( QStringList aFields );
+ bool removeRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+
+public slots:
+ void resetData();
+
+private slots:
+ void discardCurDictionary();
+
+private:
+ static const int ThumbnailSize = 25; ///< Size of image thumbnails
+
+ Dictionary* m_dictionary; // not own
+ QUndoStack* m_undoStack;
+};
+#endif
diff --git a/src/main-view/DictTableView.cpp b/src/main-view/DictTableView.cpp
new file mode 100644
index 0000000..9d61fad
--- /dev/null
+++ b/src/main-view/DictTableView.cpp
@@ -0,0 +1,84 @@
+#include "DictTableView.h"
+#include "DictTableDelegate.h"
+
+#include <QtAlgorithms>
+#include <QHeaderView>
+#include <QAbstractProxyModel>
+
+#include "DictTableModel.h"
+
+DictTableView::DictTableView( QAbstractItemModel* aModel, QWidget* aParent ):
+ QTableView( aParent )
+ {
+ setModel( aModel );
+ connect( aModel, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)),
+ SLOT(disableUpdates()) );
+ connect( aModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(enableUpdates()) );
+ setItemDelegate( new DictTableDelegate(this) );
+ setAlternatingRowColors(true);
+ setShowGrid(true);
+ verticalHeader()->setDefaultSectionSize( RowHeight );
+ setSelectionBehavior( QAbstractItemView::SelectRows );
+ resizeColumnsToContents();
+ horizontalHeader()->stretchLastSection();
+ }
+
+DictTableView::~DictTableView()
+ {
+ emit destroyed( model() );
+ }
+
+DictTableModel* DictTableView::dicTableModel() const
+ {
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model() );
+ if( tableModel )
+ return tableModel;
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model() );
+ if( proxyModel )
+ {
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( tableModel )
+ return tableModel;
+ }
+ return NULL;
+ }
+
+void DictTableView::resizeColumnsToContents()
+ {
+ QTableView::resizeColumnsToContents();
+ for( int i=0; i<model()->columnCount(); i++ )
+ if( columnWidth( i ) < KMinColWidth )
+ setColumnWidth( i, KMinColWidth );
+ else if ( columnWidth( i ) > KMaxColWidth )
+ setColumnWidth( i, KMaxColWidth );
+}
+
+void DictTableView::startEditing(int row, int col)
+{
+ edit(model()->index(row, col));
+}
+
+void DictTableView::commitEditing()
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ commitData(delegate->getEditor());
+}
+
+int DictTableView::getEditorCursorPos() const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return -1;
+ return delegate->getCursorPos();
+}
+
+void DictTableView::insertImageIntoEditor(int cursorPos, const QString& filePath) const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ delegate->insertImageIntoEditor(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableView.h b/src/main-view/DictTableView.h
new file mode 100644
index 0000000..b34df65
--- /dev/null
+++ b/src/main-view/DictTableView.h
@@ -0,0 +1,36 @@
+#ifndef DICTTABLEVIEW_h
+#define DICTTABLEVIEW_h
+
+#include <QTableView>
+#include <QtDebug>
+
+class DictTableModel;
+
+class DictTableView: public QTableView
+{
+Q_OBJECT
+
+public:
+ DictTableView( QAbstractItemModel* aModel, QWidget* aParent = 0 );
+ virtual ~DictTableView();
+ void resizeColumnsToContents();
+ DictTableModel* dicTableModel() const;
+ void startEditing(int row, int col);
+ void commitEditing();
+ int getEditorCursorPos() const;
+ void insertImageIntoEditor(int cursorPos, const QString& filePath) const;
+
+private slots:
+ void enableUpdates() { setUpdatesEnabled(true); }
+ void disableUpdates() { setUpdatesEnabled(false); }
+
+signals:
+ void destroyed( QAbstractItemModel* aDictModel );
+
+private:
+ static const int KMinColWidth = 170;
+ static const int KMaxColWidth = 400;
+ static const int RowHeight = 27;
+};
+
+#endif
diff --git a/src/main-view/DictionaryTabWidget.cpp b/src/main-view/DictionaryTabWidget.cpp
new file mode 100644
index 0000000..94f090c
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.cpp
@@ -0,0 +1,108 @@
+#include "DictionaryTabWidget.h"
+#include "MainWindow.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+
+DictionaryTabWidget::DictionaryTabWidget(MainWindow* aMainWin):
+ QTabWidget( aMainWin ), m_mainWin( aMainWin ), createdEditorsNum(0)
+ {
+ setDocumentMode( true );
+ setTabsClosable( true );
+ setMovable( true );
+ connect( this, SIGNAL(tabCloseRequested(int)), SLOT(closeTab(int)) );
+ m_undoGroup = new QUndoGroup( this );
+
+ m_continueLbl = new QLabel( this );
+ m_continueLbl->hide();
+ m_continueLbl->setPixmap(QPixmap(":/images/continue-search.png"));
+ }
+
+
+/** The tab is removed automatically when the widget is destroyed.
+ The undo stack is removed from group automatically, when the stack is destroyed.
+ */
+void DictionaryTabWidget::closeTab( int aIndex )
+ {
+ if( aIndex == -1 )
+ aIndex = currentIndex();
+ bool canRemove = m_mainWin->proposeToSave( aIndex );
+ if( canRemove )
+ delete widget( aIndex );
+ }
+
+int DictionaryTabWidget::addDictTab( DictTableModel* aDictModel )
+ {
+ DictTableView* dictView = new DictTableView( aDictModel );
+ int tabIx = addTab( dictView, "" );
+ setCurrentIndex( tabIx );
+ QUndoStack* undoStack = aDictModel->undoStack();
+ m_undoGroup->addStack( undoStack );
+ m_undoGroup->setActiveStack( undoStack );
+ connect( undoStack, SIGNAL(cleanChanged(bool)), aDictModel->dictionary(), SLOT(setContentClean(bool)) );
+ connect( dictView->itemDelegate(), SIGNAL(editorCreated()), SLOT(createEditor()) );
+ connect( dictView->itemDelegate(), SIGNAL(editorDestroyed()), SLOT(destroyEditor()) );
+ return tabIx;
+ }
+
+const DictTableView* DictionaryTabWidget::curDictView() const
+ {
+ QWidget* curWidget = currentWidget();
+ if( !curWidget )
+ return NULL;
+ DictTableView* curView = static_cast<DictTableView*>( curWidget );
+ return curView;
+ }
+
+void DictionaryTabWidget::setCurrentIndex( int aTabIx )
+ {
+ QTabWidget::setCurrentIndex( aTabIx );
+ const DictTableView* dictView = curDictView();
+ if( dictView )
+ {
+ const DictTableModel* dictModel = dictView->dicTableModel();
+ if( dictModel )
+ m_undoGroup->setActiveStack( dictModel->undoStack() );
+ }
+ }
+
+void DictionaryTabWidget::goToDictionaryRecord( int aDictIx, int aRecordRow )
+ {
+ setCurrentIndex( aDictIx );
+ QWidget* curWidget = currentWidget();
+ Q_ASSERT( curWidget );
+ QAbstractItemView* curView = static_cast<QAbstractItemView*>( curWidget );
+ Q_ASSERT( curView );
+ curView->setFocus();
+ QModelIndex index = curView->model()->index( aRecordRow, 0 );
+ curView->setCurrentIndex( index );
+ }
+
+void DictionaryTabWidget::cleanUndoStack()
+{
+ m_undoGroup->activeStack()->setClean();
+}
+
+bool DictionaryTabWidget::undoStackIsClean() const
+{
+ return m_undoGroup->isClean();
+}
+
+void DictionaryTabWidget::showContinueSearch()
+{
+ m_continueLbl->move( rect().center() - m_continueLbl->rect().center() );
+ m_continueLbl->show();
+ QTimer::singleShot( 500, m_continueLbl, SLOT(hide()) );
+}
+
+void DictionaryTabWidget::createEditor()
+{
+ createdEditorsNum++;
+ emit editingStateChanged();
+}
+
+void DictionaryTabWidget::destroyEditor()
+{
+ createdEditorsNum--;
+ emit editingStateChanged();
+}
diff --git a/src/main-view/DictionaryTabWidget.h b/src/main-view/DictionaryTabWidget.h
new file mode 100644
index 0000000..b5bd3c7
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.h
@@ -0,0 +1,46 @@
+#ifndef DICTIONARYTABWIDGET_H
+#define DICTIONARYTABWIDGET_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class MainWindow;
+class FilterBar;
+class DictTableModel;
+class DictTableView;
+class Dictionary;
+
+class DictionaryTabWidget : public QTabWidget
+{
+ Q_OBJECT
+public:
+ DictionaryTabWidget(MainWindow* aMainWin);
+
+ int addDictTab( DictTableModel* aDictModel );
+ void setCurrentIndex( int aTabIx );
+ void goToDictionaryRecord( int aDictIx, int aRecordRow );
+ void cleanUndoStack();
+ void showContinueSearch();
+
+ const DictTableView* curDictView() const;
+ QUndoGroup* undoGroup() const { return m_undoGroup; }
+ bool undoStackIsClean() const;
+ bool isInEditingState() const { return createdEditorsNum > 0; }
+
+public slots:
+ void closeTab( int aIndex = -1 );
+
+private slots:
+ void createEditor();
+ void destroyEditor();
+
+signals:
+ void editingStateChanged();
+
+private:
+ MainWindow* m_mainWin; // parent, now own
+ QUndoGroup* m_undoGroup;
+ QLabel* m_continueLbl; // Continue search icon
+ int createdEditorsNum;
+};
+#endif // DICTIONARYTABWIDGET_H
diff --git a/src/main-view/FieldContentCodec.cpp b/src/main-view/FieldContentCodec.cpp
new file mode 100644
index 0000000..ac9e6c5
--- /dev/null
+++ b/src/main-view/FieldContentCodec.cpp
@@ -0,0 +1,52 @@
+#include "FieldContentCodec.h"
+
+#include "FieldContentPainter.h"
+
+FieldContentCodec::FieldContentCodec(FieldContentPainter* painter):
+ painter(painter),
+ imageRx("<img\\s+src=\"([^\"]+)\"\\s*/?>")
+{
+ initLoopParams();
+}
+
+void FieldContentCodec::initLoopParams()
+{
+ textPos = 0;
+ prevTextPos = 0;
+}
+
+void FieldContentCodec::parse(const QString& text)
+{
+ painter->startDrawing();
+ initLoopParams();
+ this->text = text;
+
+ while(findNextImage() >= 0)
+ {
+ drawTextChunk(textPos - prevTextPos);
+ drawImage();
+ textPos += imageRx.matchedLength();
+ prevTextPos = textPos;
+ }
+ drawTextChunk(-1);
+ painter->endDrawing();
+}
+
+int FieldContentCodec::findNextImage()
+{
+ textPos = imageRx.indexIn(text, textPos);
+ return textPos;
+}
+
+void FieldContentCodec::drawTextChunk(int len)
+{
+ painter->drawText(text.mid(prevTextPos, len));
+}
+
+void FieldContentCodec::drawImage()
+{
+ QString imagePath = imageRx.cap(1);
+ if(!QFileInfo(imagePath).exists())
+ imagePath = ":/images/broken-image.png";
+ painter->drawImage(imagePath);
+}
diff --git a/src/main-view/FieldContentCodec.h b/src/main-view/FieldContentCodec.h
new file mode 100644
index 0000000..8a13dba
--- /dev/null
+++ b/src/main-view/FieldContentCodec.h
@@ -0,0 +1,28 @@
+#ifndef FIELD_CONTENT_CODEC_H
+#define FIELD_CONTENT_CODEC_H
+
+#include <QtCore>
+
+class FieldContentPainter;
+
+class FieldContentCodec
+{
+public:
+ FieldContentCodec(FieldContentPainter* painter);
+ void parse(const QString& text);
+
+private:
+ void initLoopParams();
+ int findNextImage();
+ void drawTextChunk(int len);
+ void drawImage();
+
+private:
+ FieldContentPainter* painter;
+ QString text;
+ QRegExp imageRx;
+ int textPos;
+ int prevTextPos;
+};
+#endif
+
diff --git a/src/main-view/FieldContentPainter.h b/src/main-view/FieldContentPainter.h
new file mode 100644
index 0000000..eb0679a
--- /dev/null
+++ b/src/main-view/FieldContentPainter.h
@@ -0,0 +1,16 @@
+#ifndef FIELD_CONTENT_PAINTER_H
+#define FIELD_CONTENT_PAINTER_H
+
+#include <QtCore>
+
+class FieldContentPainter
+{
+public:
+ virtual ~FieldContentPainter() {}
+ virtual void startDrawing() = 0;
+ virtual void endDrawing() = 0;
+ virtual void drawText(const QString& text) = 0;
+ virtual void drawImage(const QString& filePath) = 0;
+};
+#endif
+
diff --git a/src/main-view/FindPanel.cpp b/src/main-view/FindPanel.cpp
new file mode 100644
index 0000000..755fa50
--- /dev/null
+++ b/src/main-view/FindPanel.cpp
@@ -0,0 +1,258 @@
+#include "FindPanel.h"
+
+#include <QHBoxLayout>
+#include <QAction>
+#include <QLineEdit>
+#include <QKeyEvent>
+
+#include "DictTableView.h"
+#include "MainWindow.h"
+
+FindPanel::FindPanel( MainWindow* aMainWindow ):
+ QWidget( aMainWindow ),
+ m_mainWindow( aMainWindow ), m_direction( 1 ), m_foundOnce(false)
+ {
+ m_closeButton = new QToolButton;
+ m_closeButton->setAutoRaise(true);
+ m_closeButton->setIcon(QIcon(":/images/gray-cross.png"));
+ m_closeButton->setToolTip(tr("Close"));
+
+ QLabel* textLabel = new QLabel(tr("Find:", "Title of the find pane"));
+
+ m_textEdit = new QComboBox;
+ textLabel->setBuddy(m_textEdit);
+ m_textEdit->setEditable(true);
+ m_textEdit->setInsertPolicy( QComboBox::NoInsert );
+ m_textEdit->setMaxCount( ComboBoxMaxItems );
+ m_textEdit->setMinimumWidth( TextEditMinWidth );
+ m_textEdit->setFocus();
+
+ m_findBackwardBtn = new QToolButton;
+ m_findBackwardBtn->resize(32, 32);
+ m_findBackwardBtn->setObjectName("backward");
+ m_findBackwardBtn->setIcon(QIcon(":/images/1leftarrow.png"));
+ m_findBackwardBtn->setToolTip(tr("Find previous"));
+ m_findBackwardBtn->setEnabled(false);
+
+ m_findForwardBtn = new QToolButton;
+ m_findForwardBtn->setObjectName("forward");
+ m_findForwardBtn->setIcon(QIcon(":/images/1rightarrow.png"));
+ m_findForwardBtn->setToolTip(tr("Find next"));
+ m_findForwardBtn->setEnabled(false);
+
+ m_caseSensitiveBtn = new QToolButton;
+ m_caseSensitiveBtn->setAutoRaise(true);
+ m_caseSensitiveBtn->setCheckable( true );
+ m_caseSensitiveBtn->setIcon(QIcon(":/images/Aa.png"));
+ m_caseSensitiveBtn->setToolTip(tr("Case sensitive"));
+
+ m_wholeWordsBtn = new QToolButton;
+ m_wholeWordsBtn->setAutoRaise(true);
+ m_wholeWordsBtn->setCheckable( true );
+ m_wholeWordsBtn->setIcon(QIcon(":/images/whole-words.png"));
+ m_wholeWordsBtn->setToolTip(tr("Whole words"));
+
+ m_regExpBtn = new QToolButton;
+ m_regExpBtn->setAutoRaise(true);
+ m_regExpBtn->setCheckable( true );
+ m_regExpBtn->setIcon(QIcon(":/images/RX.png"));
+ m_regExpBtn->setToolTip(tr("Regular expression"));
+
+ m_inSelectionBtn = new QToolButton;
+ m_inSelectionBtn->setAutoRaise(true);
+ m_inSelectionBtn->setCheckable( true );
+ m_inSelectionBtn->setIcon(QIcon(":/images/selection.png"));
+ m_inSelectionBtn->setToolTip(tr("In selection"));
+
+ QLabel* infoIconLbl = new QLabel;
+ infoIconLbl->setPixmap( QPixmap(":/images/warning.png").scaled(
+ 16, 16, Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
+ m_infoLbl = new QLabel(tr("String is not found"));
+ QHBoxLayout* infoLt = new QHBoxLayout;
+ infoLt->setContentsMargins( QMargins() );
+ infoLt->addWidget( infoIconLbl );
+ infoLt->addWidget( m_infoLbl );
+ m_infoPane = new QWidget;
+ m_infoPane->setLayout( infoLt );
+ m_infoPane->hide();
+
+ QHBoxLayout* mainLayout = new QHBoxLayout;
+ mainLayout->addWidget( m_closeButton );
+ mainLayout->addWidget( textLabel );
+ mainLayout->addWidget( m_textEdit );
+ mainLayout->addWidget( m_findBackwardBtn );
+ mainLayout->addWidget( m_findForwardBtn );
+ mainLayout->addWidget( m_caseSensitiveBtn );
+ mainLayout->addWidget( m_wholeWordsBtn );
+ mainLayout->addWidget( m_regExpBtn );
+ mainLayout->addWidget( m_inSelectionBtn );
+ mainLayout->addSpacing( 50 );
+ mainLayout->addWidget( m_infoPane );
+ mainLayout->addStretch();
+ mainLayout->setContentsMargins( QMargins() );
+ setLayout( mainLayout );
+
+ connect( m_inSelectionBtn, SIGNAL(toggled(bool)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), m_infoPane, SLOT(hide()) );
+ connect( m_findForwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_findBackwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_closeButton, SIGNAL(clicked()), this, SLOT(hide()) );
+ }
+
+void FindPanel::show()
+ {
+ m_textEdit->setFocus();
+ m_textEdit->lineEdit()->selectAll();
+ QWidget::show();
+ }
+
+void FindPanel::keyPressEvent( QKeyEvent* event )
+ {
+ if( event->key() == Qt::Key_Return )
+ find();
+ else
+ QWidget::keyPressEvent( event );
+ }
+
+/**
+ @arg aDirection 1 = forward; -1 = backward; 0 = previous direction
+ */
+void FindPanel::find()
+ {
+ // Check controls
+ QString searchText = m_textEdit->currentText();
+ if( searchText.isEmpty() )
+ return;
+
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndexList rows = tableView->selectionModel()->selectedRows();
+ if( rows.size() <= 1 && m_inSelectionBtn->isChecked() )
+ m_inSelectionBtn->setChecked( false );
+
+ // Process search parameters
+ bool inSelection = m_inSelectionBtn->isChecked();
+ bool fromCursor = true;
+ if( inSelection || (rows.size() == 1 && rows.first().row() == 0 ) )
+ fromCursor = false;
+
+ // Process direction
+ if( sender() )
+ {
+ if( sender()->objectName() == "forward" )
+ m_direction = 1;
+ else if( sender()->objectName() == "backward" )
+ m_direction = -1;
+ }
+ // For "find again" case, the direction stays the same
+
+ // Save the entered text to combobox
+ int textIndex = m_textEdit->findText( searchText );
+ if( textIndex != 0 ) // Don't re-add the same text at the first line
+ {
+ if( textIndex > -1 ) // Remove duplicates
+ m_textEdit->removeItem( textIndex );
+ m_textEdit->insertItem( 0, searchText );
+ }
+
+ // Create the search regular expression
+ QString searchRegExpStr;
+ if( !m_regExpBtn->isChecked() )
+ searchRegExpStr = QRegExp::escape( searchText );
+ else
+ searchRegExpStr = searchText;
+ if( m_wholeWordsBtn->isChecked() )
+ searchRegExpStr = "\\b" + searchText + "\\b";
+
+ QRegExp searchRegExp = QRegExp( searchRegExpStr,
+ m_caseSensitiveBtn->isChecked()? Qt::CaseSensitive : Qt::CaseInsensitive);
+
+ // Get sorted search range
+ QModelIndexList searchRange;
+ if( inSelection )
+ {
+ searchRange = tableView->selectionModel()->selectedIndexes();
+ qSort( searchRange );
+ }
+ else // all indexes
+ {
+ QAbstractItemModel* tableModel = tableView->model();
+ for(int r=0; r < tableModel->rowCount(); r++)
+ for(int c=0; c < tableModel->columnCount(); c++)
+ searchRange << tableModel->index(r, c);
+ }
+
+ // Get the starting search point (iterator)
+ QListIterator<QModelIndex> startingPoint( searchRange );
+ if( fromCursor )
+ {
+ bool ok = startingPoint.findNext( tableView->currentIndex() );
+ if( !ok )
+ startingPoint.toFront();
+ if( ok && m_direction < 0 )
+ startingPoint.previous(); // Go one item backwards
+ }
+ else
+ if( m_direction < 0 )
+ startingPoint.toBack();
+
+ // Try to find the regexp
+ bool found = findRegExp( searchRegExp, startingPoint );
+ if ( !found && fromCursor )
+ {
+ // Continue searching
+ m_mainWindow->showContinueSearch();
+ if( m_direction > 0 )
+ startingPoint.toFront();
+ else
+ startingPoint.toBack();
+ if( findRegExp( searchRegExp, startingPoint ) )
+ found = true;
+ }
+ if( !found )
+ m_infoPane->show();
+ }
+
+bool FindPanel::findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint )
+ {
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndex foundIndex;
+ while( (m_direction > 0)? aStartingPoint.hasNext() : aStartingPoint.hasPrevious() )
+ {
+ QModelIndex index = (m_direction > 0)? aStartingPoint.next() : aStartingPoint.previous();
+ QString valueStr = index.data( Qt::EditRole ).toString(); // Search in display, not edit, strings. Matters for <img>.
+ if( valueStr.contains( aSearchRegExp ) )
+ {
+ foundIndex = index;
+ break;
+ }
+ }
+ if( foundIndex.isValid() )
+ {
+ tableView->setFocus();
+ tableView->setCurrentIndex( foundIndex );
+ return true;
+ }
+ else
+ return false;
+ }
+
+void FindPanel::updateFindButtons()
+ {
+ m_findForwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() );
+ m_findBackwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() && !m_inSelectionBtn->isChecked() );
+ }
+
+bool FindPanel::canFindAgain()
+ {
+ if( !m_foundOnce )
+ return false;
+ const DictTableView* tableView = m_mainWindow->getCurDictView();
+ if( tableView && !m_textEdit->currentText().isEmpty() )
+ return true;
+ else
+ return false;
+ }
+
+
+
diff --git a/src/main-view/FindPanel.h b/src/main-view/FindPanel.h
new file mode 100644
index 0000000..553522e
--- /dev/null
+++ b/src/main-view/FindPanel.h
@@ -0,0 +1,66 @@
+#ifndef FINDPANEL_H
+#define FINDPANEL_H
+
+#include <QWidget>
+#include <QString>
+#include <QModelIndex>
+#include <QComboBox>
+#include <QPushButton>
+#include <QToolButton>
+#include <QCheckBox>
+#include <QMenu>
+#include <QLabel>
+
+class DictTableView;
+class MainWindow;
+
+/**
+ * Modern find panel for dictionary records.
+ */
+
+class FindPanel: public QWidget
+{
+ Q_OBJECT
+public:
+ FindPanel( MainWindow* aMainWindow );
+
+ void setTableView( DictTableView* aTableView );
+ bool canFindAgain();
+
+protected:
+ void keyPressEvent( QKeyEvent* event );
+
+private:
+ bool findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint );
+
+public slots:
+ void show();
+ void find();
+
+private slots:
+ void updateFindButtons();
+
+private:
+ static const int ComboBoxMaxItems = 10;
+ static const int TextEditMinWidth = 300;
+
+private:
+ // Data
+ MainWindow* m_mainWindow;
+ int m_direction; // Search direction: 1 or -1
+ bool m_foundOnce; // The search text was already found once. For "Find again".
+
+ // GUI
+ QToolButton* m_closeButton;
+ QComboBox* m_textEdit;
+ QToolButton* m_findForwardBtn;
+ QToolButton* m_findBackwardBtn;
+ QToolButton* m_caseSensitiveBtn;
+ QToolButton* m_wholeWordsBtn;
+ QToolButton* m_regExpBtn;
+ QToolButton* m_inSelectionBtn;
+ QWidget* m_infoPane;
+ QLabel* m_infoLbl;
+};
+
+#endif
diff --git a/src/main-view/LanguageMenu.cpp b/src/main-view/LanguageMenu.cpp
new file mode 100644
index 0000000..84ffd46
--- /dev/null
+++ b/src/main-view/LanguageMenu.cpp
@@ -0,0 +1,64 @@
+#include "LanguageMenu.h"
+#include "../main.h"
+#include "../strings.h"
+
+LanguageMenu::LanguageMenu(QMenu* parent):
+ QMenu(tr("&Language"), parent),
+ locale(QSettings().value("lang").toString()),
+ systemLocale(QLocale::system().name().split("_").first())
+{
+ initUi(parent);
+ initLangs();
+ createActionsGroup();
+ foreach(QString key, langs.keys())
+ createAction(key);
+}
+
+void LanguageMenu::initUi(QMenu* parent)
+{
+ parent->addMenu(this);
+ setIcon(QIcon(":/images/language.png"));
+}
+
+void LanguageMenu::initLangs()
+{
+ langs["cs"] = "Čeština (Czech)";
+ langs["fi"] = "Suomi (Finnish)";
+ langs["fr"] = "Français (French)";
+ langs["de"] = "Deutsch (German)";
+ langs["en"] = "English";
+ langs["ru"] = "Русский (Russian)";
+ langs["es"] = "Español (Spanish)";
+ langs["uk"] = "Українська (Ukrainian)";
+ langs[""] = tr("System") + ": " + langs[systemLocale];
+}
+
+void LanguageMenu::createActionsGroup()
+{
+ actionsGroup = new QActionGroup(this);
+ actionsGroup->setExclusive(true);
+}
+
+void LanguageMenu::createAction(const QString& key)
+{
+ QAction* action = addAction(langs[key]);
+ action->setCheckable(true);
+ if(key == locale)
+ action->setChecked(true);
+ action->setData(key);
+ actionsGroup->addAction(action);
+ connect(action, SIGNAL(triggered()), SLOT(saveLanguage()));
+}
+
+void LanguageMenu::saveLanguage()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ QString newLocale = action->data().toString();
+ QSettings().setValue("lang", newLocale);
+ if(newLocale.isEmpty())
+ newLocale = systemLocale;
+
+ installTranslator("freshmemory_" + newLocale, getResourcePath() + "/tr");
+ QMessageBox::information(this, Strings::tr(Strings::s_appTitle),
+ tr("The application must be restarted to use the selected language"));
+}
diff --git a/src/main-view/LanguageMenu.h b/src/main-view/LanguageMenu.h
new file mode 100644
index 0000000..d03984d
--- /dev/null
+++ b/src/main-view/LanguageMenu.h
@@ -0,0 +1,28 @@
+#ifndef LANGUAGE_MENU_H
+#define LANGUAGE_MENU_H
+
+#include <QtWidgets>
+
+class LanguageMenu: public QMenu
+{
+ Q_OBJECT
+public:
+ LanguageMenu(QMenu* parent);
+
+private:
+ void initUi(QMenu* parent);
+ void initLangs();
+ void createActionsGroup();
+ void createAction(const QString& key);
+
+private slots:
+ void saveLanguage();
+
+private:
+ QMap<QString, QString> langs;
+ QString locale;
+ QString systemLocale;
+ QActionGroup* actionsGroup;
+};
+
+#endif
diff --git a/src/main-view/MainWindow.cpp b/src/main-view/MainWindow.cpp
new file mode 100644
index 0000000..e5b285c
--- /dev/null
+++ b/src/main-view/MainWindow.cpp
@@ -0,0 +1,1313 @@
+#include "MainWindow.h"
+#include "AppModel.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "FindPanel.h"
+#include "DictTableDelegate.h"
+#include "PacksTreeModel.h"
+#include "AboutDialog.h"
+#include "RecentFilesManager.h"
+#include "LanguageMenu.h"
+#include "CardPreview.h"
+#include "WelcomeScreen.h"
+#include "../version.h"
+#include "../strings.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/DicCsvWriter.h"
+#include "../dictionary/DicCsvReader.h"
+#include "../dic-options/DictionaryOptionsDialog.h"
+#include "../export-import/CsvExportDialog.h"
+#include "../export-import/CsvImportDialog.h"
+#include "../settings/FontColorSettingsDialog.h"
+#include "../settings/StudySettingsDialog.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../study/WordDrillWindow.h"
+#include "../study/SpacedRepetitionWindow.h"
+#include "../study/CardSideView.h"
+#include "../utils/RandomGenerator.h"
+#include "../statistics/StatisticsView.h"
+
+MainWindow::MainWindow(AppModel* model):
+ workPath(""),
+ addImagePath(""),
+ model(model), welcomeScreen(NULL), studyWindow(NULL)
+{
+ init();
+ createCentralWidget();
+ createActions();
+ createMenus();
+ createToolBars();
+ createStatusBar();
+ FieldStyleFactory::inst()->load();
+ createDockWindows();
+ readSettings();
+ openSession();
+}
+
+MainWindow::~MainWindow()
+{
+ delete studyWindow;
+}
+
+void MainWindow::init()
+{
+ setWindowTitle(Strings::tr(Strings::s_appTitle));
+ setWindowIcon(QIcon(":/images/freshmemory.png"));
+}
+
+void MainWindow::activate()
+{
+ activateWindow();
+ raise();
+}
+
+void MainWindow::updateDictTab()
+{
+ int curIndex = dictTabWidget->currentIndex();
+ const Dictionary* curDict = model->curDictionary();
+ if(!curDict)
+ return;
+ if(!curDict->contentModified())
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/openbook-24.png") );
+ else
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/filesave.png") );
+ dictTabWidget->setTabText(curIndex, curDict->shortName());
+ dictTabWidget->setTabToolTip(curIndex, QDir::toNativeSeparators( curDict->getFilePath()));
+}
+
+void MainWindow::updateTotalRecordsLabel()
+{
+ const Dictionary* curDict = model->curDictionary();
+ if(curDict)
+ {
+ totalRecordsLabel->show();
+ totalRecordsLabel->setText( tr("Records: %1").arg(curDict->entriesNum()) );
+ }
+ else
+ totalRecordsLabel->hide();
+}
+
+void MainWindow::updateActions()
+{
+ const Dictionary* curDict = model->curDictionary();
+ bool isCurDictModified = curDict && curDict->contentModified();
+
+ saveAct->setEnabled( isCurDictModified );
+ saveAsAct->setEnabled( curDict );
+ saveCopyAct->setEnabled( curDict );
+ exportAct->setEnabled( curDict );
+ removeTabAct->setEnabled( curDict );
+ updatePasteAction();
+ insertRecordsAct->setEnabled( curDict );
+ findAct->setEnabled( curDict );
+ if( findPanel )
+ findAgainAct->setEnabled( findPanel->canFindAgain() );
+ wordDrillAct->setEnabled(curDict);
+ spacedRepetitionAct->setEnabled(curDict);
+ statisticsAct->setEnabled(curDict);
+ dictionaryOptionsAct->setEnabled(curDict);
+}
+
+void MainWindow::updateAddImageAction()
+{
+ addImageAct->setEnabled(dictTabWidget->isInEditingState());
+}
+
+void MainWindow::updateSelectionActions()
+ {
+ if( !model->curDictionary() )
+ return;
+ const DictTableView* tableView = getCurDictView();
+ if( !tableView )
+ return;
+ Q_ASSERT( tableView->selectionModel() );
+ bool hasSelection = tableView->selectionModel()->hasSelection();
+ foreach( QAction* action, selectionActions )
+ action->setEnabled( hasSelection );
+ }
+
+void MainWindow::updatePasteAction()
+{
+ bool isCurTabValid = dictTabWidget->currentIndex() >= 0;
+ bool hasClipboardText = !QApplication::clipboard()->text().isEmpty();
+ pasteAct->setEnabled( isCurTabValid && hasClipboardText );
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+if( proposeToSave() )
+ {
+ writeSettings();
+ event->accept();
+ qApp->quit();
+ }
+else
+ event->ignore();
+}
+
+void MainWindow::newFile()
+ {
+ addDictTab(model->newDictionary());
+ updatePacksTreeView();
+ }
+
+void MainWindow::openFileWithDialog()
+ {
+ openFileWithDialog(workPath);
+ }
+
+void MainWindow::openOnlineDictionaries()
+ {
+ QDesktopServices::openUrl(QUrl("http://fresh-memory.com/dictionaries"));
+ }
+
+void MainWindow::openFileWithDialog(const QString& dirPath)
+ {
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Open dictionary"), dirPath,
+ tr("Dictionaries", "Filter name in dialog")+ " (*.fmd)");
+ if( filePath.isEmpty() )
+ return;
+ openFile(filePath);
+ }
+
+void MainWindow::openFile(const QString& filePath)
+ {
+ QString internalFilePath = QDir::fromNativeSeparators(filePath);
+ workPath = QFileInfo(internalFilePath).path();
+ int dictIndex = model->indexOfDictionary(internalFilePath);
+ if(isDictOpened(dictIndex))
+ setCurDictionary(dictIndex);
+ else
+ if(!loadFile(internalFilePath))
+ return;
+ updateAfterOpenedDictionary(internalFilePath);
+ }
+
+bool MainWindow::isDictOpened(int index)
+{
+ return index > -1;
+}
+
+void MainWindow::setCurDictionary(int index)
+{
+ if(index == -1)
+ return;
+ model->setCurDictionary(index);
+ setCurDictTab(index);
+}
+
+bool MainWindow::loadFile(const QString& filePath)
+{
+ bool ok = model->openDictionary(filePath);
+ if(ok)
+ addDictTab(model->curDictionary());
+ else
+ showOpenFileError();
+ return ok;
+}
+
+void MainWindow::showOpenFileError()
+{
+ QMessageBox::critical(this, Strings::errorTitle(), model->getErrorMessage());
+}
+
+void MainWindow::updateAfterOpenedDictionary(const QString& filePath)
+{
+ recentFilesMan->addFile(filePath);
+ updatePacksTreeView();
+ updateCardPreview();
+}
+
+void MainWindow::addDictTab(Dictionary* dict)
+ {
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateActions()) );
+ connect( dict, SIGNAL(studyModifiedChanged(bool)), SLOT(saveStudyWithDelay(bool)) );
+ connect( dict, SIGNAL(filePathChanged()), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(entriesInserted(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(entriesRemoved(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updatePacksTreeView()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updateCardPreview()) );
+ connect( dict, SIGNAL(destroyed()), SLOT(updatePacksTreeView()) );
+
+ dictTabWidget->addDictTab(model->curDictModel());
+ DictTableView* tableView = const_cast<DictTableView*>( dictTabWidget->curDictView() );
+
+ tableView->addActions(contextMenuActions);
+ tableView->verticalHeader()->addActions(contextMenuActions);
+ tableView->setContextMenuPolicy( Qt::ActionsContextMenu );
+ tableView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu );
+
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateSelectionActions()) );
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateCardPreview()) );
+ connect( tableView, SIGNAL(destroyed(QAbstractItemModel*)), model, SLOT(removeDictModel(QAbstractItemModel*)) );
+ updateDictTab();
+ updateTotalRecordsLabel();
+ updateSelectionActions();
+ packsTreeView->reset();
+ QModelIndex firstIx = tableView->model()->index(0, 0, QModelIndex());
+ tableView->setCurrentIndex(firstIx);
+ tableView->setFocus();
+ }
+
+const DictTableView* MainWindow::getCurDictView() const
+ {
+ if(studyWindow)
+ {
+ const DictTableView* editCardView = studyWindow->cardEditView();
+ if( editCardView )
+ return editCardView;
+ }
+ return dictTabWidget->curDictView();
+ }
+
+/// Fork to SaveAs() or really save in DoSave()
+bool MainWindow::Save()
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() || dict->nameIsTemp() )
+ return SaveAs();
+else
+ return doSave( filePath );
+}
+
+bool MainWindow::SaveAs( bool aChangeFilePath )
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() )
+ filePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+filePath = QFileDialog::getSaveFileName(this, tr("Save dictionary as ..."), filePath);
+if( filePath.isEmpty() )
+ return false;
+workPath = QFileInfo( filePath ).path();
+if( doSave( filePath, aChangeFilePath ) )
+ {
+ recentFilesMan->addFile(filePath);
+ return true;
+ }
+else
+ return false;
+}
+
+void MainWindow::SaveCopy()
+{
+ SaveAs( false ); // Do not change the file name
+}
+
+void MainWindow::importFromCsv()
+{
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Import CSV file"), workPath);
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ CsvImportDialog importDialog( this, filePath, model );
+ if( importDialog.exec() )
+ {
+ model->addDictionary( importDialog.getDictionary() );
+ addDictTab(importDialog.getDictionary());
+ importDialog.getDictionary()->generateCards();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::exportToCsv()
+{
+ Dictionary* dict = model->curDictionary();
+ if( !dict )
+ return;
+ CsvExportDialog exportDialog( this, dict );
+ if( exportDialog.exec() )
+ {
+ QString dictFilePath( dict->getFilePath() );
+ if( dictFilePath.isEmpty() )
+ dictFilePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+ QString filePath = QFileDialog::getSaveFileName( this, tr("Export to CSV file"), dictFilePath + ".txt" );
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ exportDialog.SaveCSVToFile( filePath );
+ }
+}
+
+bool MainWindow::doSave( const QString& aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = model->curDictionary()->save( aFilePath, aChangeFilePath );
+ if( error != QFile::NoError )
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), (tr("Cannot save dictionary:") + "\n%1").
+ arg(QDir::toNativeSeparators(aFilePath)) );
+ return false;
+ }
+ dictTabWidget->cleanUndoStack();
+ updateDictTab();
+ updateActions();
+ return true;
+}
+
+bool MainWindow::saveStudy()
+{
+ QTime saveTime;
+ saveTime.start();
+ Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return false;
+ if(dict->saveStudy() != QFile::NoError)
+ {
+ QMessageBox::warning(this, Strings::errorTitle(),
+ (tr("Cannot save study file:") + "\n%1").
+ arg(QDir::toNativeSeparators(
+ model->curDictionary()->getStudyFilePath())));
+ return false;
+ }
+ return true;
+}
+
+void MainWindow::saveStudyWithDelay(bool studyModified)
+{
+ if(studyModified)
+ QTimer::singleShot(AutoSaveStudyInterval, this, SLOT(saveStudy()));
+}
+
+/**
+ * Checks if all files are saved. If not, proposes to the user to save them.
+ * This function is called before closing the application.
+ * @return true, if all files are now saved and the application can be closed
+ */
+bool MainWindow::proposeToSave()
+{
+for(int i=0; i<dictTabWidget->count(); i++)
+ if( !proposeToSave(i) )
+ return false;
+return true;
+}
+
+/**
+ * Checks if the specified file is modified and if it is, proposes to the user to save it.
+ * If the specified file is not modified, the function just returns true.
+ *
+ * Function silently saves the study data, if it is modified.
+ *
+ * @param aTabIndex Index of the tab containing the modified file
+ * @return true, if the file is saved and the application can be closed.
+ * false, if the user declined the proposition to save the file.
+ */
+bool MainWindow::proposeToSave(int aTabIndex)
+{
+const Dictionary* dict = model->dictionary( aTabIndex );
+
+if( dict->contentModified() )
+ {
+ dictTabWidget->setCurrentIndex( aTabIndex );
+ QMessageBox::StandardButton pressedButton;
+ pressedButton = QMessageBox::warning( this, tr("Save dictionary?"), tr("Dictionary %1 was modified.\n"
+ "Save changes?").arg( dict->shortName(false) ),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel );
+ if( pressedButton == QMessageBox::Yes )
+ {
+ bool saved = Save();
+ if( !saved )
+ return false;
+ }
+ else if( pressedButton == QMessageBox::Cancel )
+ return false;
+ }
+if(dict->studyModified())
+ saveStudy();
+return true;
+}
+
+void MainWindow::openSession()
+{
+ QSettings settings;
+ QStringList sessionFiles = settings.value("session").toStringList();
+ if(sessionFiles.isEmpty())
+ {
+ QString lastFile = recentFilesMan->getLastUsedFilePath();
+ if(lastFile.isEmpty())
+ return;
+ sessionFiles << recentFilesMan->getLastUsedFilePath();
+ }
+ foreach(QString fileName, sessionFiles)
+ openFile(fileName);
+ int curTab = settings.value("session-cur-tab").toInt();
+ setCurDictionary(curTab);
+}
+
+void MainWindow::help()
+ {
+ QString fullVersion = QString(FM_VERSION);
+ QString version = fullVersion.left(fullVersion.lastIndexOf('.'));
+ QString url = QString("http://fresh-memory.com/docs/%1/index.html").arg(version);
+ QDesktopServices::openUrl(QUrl(url));
+ }
+
+void MainWindow::about()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+ dialog.setParent(NULL);
+}
+
+void MainWindow::addImage()
+ {
+ int cursorPos = getCurEditorCursorPos();
+ if(cursorPos < 0)
+ return;
+ const_cast<DictTableView*>(getCurDictView())->commitEditing();
+ QString selectedFile = selectAddImageFile();
+ if(selectedFile.isEmpty())
+ return;
+ selectedFile = copyImageFileToImagesDir(selectedFile);
+ if(selectedFile.isEmpty())
+ return;
+ insertImageIntoCurEditor(cursorPos, selectedFile);
+ }
+
+QString MainWindow::selectAddImageFile()
+ {
+ checkAddImagePath();
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Add image"),
+ addImagePath,
+ tr("Images", "Filter name in dialog") +
+ " (*.png *.jpg *.jpeg *.gif *.svg *.xpm *.ico *.mng *.tiff);; " +
+ tr("All files") + " (*.*)");
+ if(filePath.isEmpty())
+ return QString();
+ addImagePath = QFileInfo(filePath).path();
+ return filePath;
+ }
+
+void MainWindow::checkAddImagePath()
+{
+ if(!addImagePath.isEmpty())
+ return;
+ if(!getCurDict())
+ return;
+ QString dicFilePath = getCurDict()->getFilePath();
+ addImagePath = QFileInfo(dicFilePath).path();
+}
+
+QString MainWindow::copyImageFileToImagesDir(const QString& filePath)
+{
+ QString dicImagesDir = getCurDict()->getImagesPath();
+ QString imageDir = QFileInfo(filePath).path();
+ if(imageDir == dicImagesDir)
+ return filePath;
+ QString newFilePath = createNewImageFilePath(dicImagesDir, filePath);
+ if(!QFileInfo(dicImagesDir).exists())
+ QDir(dicImagesDir).mkpath(".");
+ if(!QFile::copy(filePath, newFilePath))
+ return QString();
+ return newFilePath;
+}
+
+QString MainWindow::createNewImageFilePath(const QString& dicImagesDir,
+ const QString& filePath)
+{
+ int num = 0;
+ QString newFilePath;
+ do
+ {
+ newFilePath = createImagesDirFilePath(dicImagesDir, filePath, num);
+ num++;
+ }
+ while(QFileInfo(newFilePath).exists());
+ return newFilePath;
+}
+
+QString MainWindow::createImagesDirFilePath(const QString& dicImagesDir,
+ const QString& filePath, int suffixNum)
+{
+ QString imageFileName = QFileInfo(filePath).completeBaseName();
+ if(suffixNum > 0)
+ imageFileName += "-" + QString::number(suffixNum);
+ imageFileName += "." + QFileInfo(filePath).suffix();
+ return QDir(dicImagesDir).filePath(imageFileName);
+}
+
+int MainWindow::getCurEditorCursorPos()
+ {
+ const DictTableView* dictView = getCurDictView();
+ if(!dictView)
+ return -1;
+ return dictView->getEditorCursorPos();
+ }
+
+int MainWindow::getCurRow()
+{
+ return getCurDictView()->currentIndex().row();
+}
+
+int MainWindow::getCurColumn()
+{
+ return getCurDictView()->currentIndex().column();
+}
+
+Dictionary* MainWindow::getCurDict()
+{
+ return model->curDictionary();
+}
+
+void MainWindow::insertImageIntoCurEditor(int cursorPos, const QString& filePath)
+{
+ const DictTableView* dictView = getCurDictView();
+ const_cast<DictTableView*>(dictView)->startEditing(getCurRow(), getCurColumn());
+ dictView->insertImageIntoEditor(cursorPos, filePath);
+}
+
+void MainWindow::insertRecords()
+ {
+ pushToUnoStack(new InsertRecordsCmd(this));
+ }
+
+void MainWindow::removeRecords()
+ {
+ pushToUnoStack(new RemoveRecordsCmd(this));
+ }
+
+void MainWindow::pushToUnoStack(QUndoCommand* command)
+ {
+ getCurDictView()->dicTableModel()->undoStack()->push(command);
+ }
+
+void MainWindow::find()
+ {
+ findPanel->show();
+ findAgainAct->setEnabled(true);
+ }
+
+void MainWindow::findAgain()
+ {
+ findPanel->find();
+ }
+
+void MainWindow::createActions()
+{
+ createFileActions();
+ createEditActions();
+ createDictContextMenuActions();
+ createSelectionActions();
+ createToolsActions();
+ createSettingsActions();
+ createHelpActions();
+ initActions();
+}
+
+void MainWindow::createFileActions()
+{
+ newAct = new QAction(QIcon(":/images/filenew.png"), tr("&New"), this);
+ newAct->setShortcut(tr("Ctrl+N"));
+ connect(newAct, SIGNAL(triggered()), SLOT(newFile()));
+
+ loadAct = new QAction(QIcon(":/images/fileopen.png"), tr("&Open ..."), this);
+ loadAct->setShortcut(tr("Ctrl+O"));
+ connect(loadAct, SIGNAL(triggered()), SLOT(openFileWithDialog()));
+
+ openFlashcardsAct = new QAction(tr("Online dictionaries"), this);
+ connect(openFlashcardsAct, SIGNAL(triggered()), SLOT(openOnlineDictionaries()));
+
+ saveAct = new QAction(QIcon(":/images/filesave.png"), tr("&Save"), this);
+ saveAct->setShortcut(tr("Ctrl+S"));
+ connect(saveAct, SIGNAL(triggered()), SLOT(Save()));
+
+ saveAsAct = new QAction(QIcon(":/images/filesaveas.png"), tr("Save &as ..."), this);
+ connect(saveAsAct, SIGNAL(triggered()), SLOT(SaveAs()));
+
+ saveCopyAct = new QAction(tr("Save &copy ..."), this);
+ connect(saveCopyAct, SIGNAL(triggered()), SLOT(SaveCopy()));
+
+ importAct = new QAction(tr("&Import from CSV ..."), this);
+ connect(importAct, SIGNAL(triggered()), SLOT(importFromCsv()));
+
+ exportAct = new QAction(tr("&Export to CSV ..."), this);
+ connect(exportAct, SIGNAL(triggered()), SLOT(exportToCsv()));
+
+ removeTabAct = new QAction(QIcon(":/images/remove.png"),
+ tr("&Close dictionary"), this);
+ removeTabAct->setShortcut(tr("Ctrl+W"));
+ connect(removeTabAct, SIGNAL(triggered()), dictTabWidget, SLOT(closeTab()));
+
+ quitAct = new QAction(QIcon(":/images/exit.png"), tr("&Quit"), this);
+ quitAct->setShortcut(tr("Ctrl+Q"));
+ connect(quitAct, SIGNAL(triggered()), SLOT(close()));
+}
+
+void MainWindow::createEditActions()
+{
+ undoAct = dictTabWidget->undoGroup()->createUndoAction(this);
+ undoAct->setShortcut(tr("Ctrl+Z"));
+
+ redoAct = dictTabWidget->undoGroup()->createRedoAction(this);
+ redoAct->setShortcut(tr("Ctrl+Y"));
+
+ copyAct = new QAction(QIcon(":/images/editcopy.png"), tr("&Copy"), this);
+ copyAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+C") << QString("Ctrl+Insert"));
+ connect(copyAct, SIGNAL(triggered()), SLOT(copyEntries()));
+
+ cutAct = new QAction(QIcon(":/images/editcut.png"), tr("Cu&t"), this);
+ cutAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+X") << QString("Shift+Delete"));
+ connect(cutAct, SIGNAL(triggered()), SLOT(cutEntries()));
+
+ pasteAct = new QAction(QIcon(":/images/editpaste.png"), tr("&Paste"), this);
+ pasteAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+V") << QString("Shift+Insert"));
+ connect(pasteAct, SIGNAL(triggered()), SLOT(pasteEntries()));
+ connect(QApplication::clipboard(), SIGNAL(dataChanged()), SLOT(updatePasteAction()));
+
+ createAddImageAct();
+ createinsertRecordsAct();
+ createRemoveRecordsAct();
+
+ findAct = new QAction(QIcon(":/images/find.png"), tr("&Find..."), this);
+ findAct->setShortcut(tr("Ctrl+F"));
+ connect(findAct, SIGNAL(triggered()), SLOT(find()));
+
+ findAgainAct = new QAction(QIcon(":/images/next.png"), tr("Find &again"), this);
+ findAgainAct->setShortcut(QString("F3"));
+ connect(findAgainAct, SIGNAL(triggered()), SLOT(findAgain()));
+}
+
+void MainWindow::createAddImageAct()
+{
+ addImageAct = new QAction(QIcon(":/images/add-image.png"), tr("&Add image"), this);
+ addImageAct->setShortcut(tr("Ctrl+G"));
+ connect(addImageAct, SIGNAL(triggered()), SLOT(addImage()));
+ connect(dictTabWidget, SIGNAL(editingStateChanged()), SLOT(updateAddImageAction()));
+}
+
+void MainWindow::createinsertRecordsAct()
+{
+ insertRecordsAct = new QAction(QIcon(":/images/add.png"),
+ tr("&Insert record"), this);
+ insertRecordsAct->setShortcut(tr("Ctrl+I"));
+ connect(insertRecordsAct, SIGNAL(triggered()), SLOT(insertRecords()));
+}
+
+void MainWindow::createRemoveRecordsAct()
+{
+ removeRecordsAct = new QAction(QIcon(":/images/delete.png"),
+ tr("&Remove record"), this);
+ removeRecordsAct->setShortcut(Qt::Key_Delete);
+ connect(removeRecordsAct, SIGNAL(triggered()), SLOT(removeRecords()));
+}
+
+void MainWindow::createToolsActions()
+{
+ QSignalMapper* startStudySignalMapper = new QSignalMapper(this);
+ connect(startStudySignalMapper, SIGNAL(mapped(int)), SLOT(startStudy(int)));
+
+ wordDrillAct = new QAction(QIcon(":/images/word-drill.png"), tr("&Word drill"),
+ this);
+ wordDrillAct->setShortcut(QString("F5"));
+ connect(wordDrillAct, SIGNAL(triggered()), startStudySignalMapper, SLOT(map()));
+ startStudySignalMapper->setMapping(wordDrillAct, AppModel::WordDrill);
+
+ spacedRepetitionAct = new QAction(QIcon(":/images/spaced-rep.png"),
+ tr("&Spaced repetition"), this);
+ spacedRepetitionAct->setShortcut(QString("F6"));
+ connect(spacedRepetitionAct, SIGNAL(triggered()), startStudySignalMapper,
+ SLOT(map()));
+ startStudySignalMapper->setMapping(spacedRepetitionAct, AppModel::SpacedRepetition);
+
+ statisticsAct = new QAction(QIcon(":/images/statistics.png"), tr("S&tatistics"),
+ this);
+ statisticsAct->setShortcut(QString("F7"));
+ connect(statisticsAct, SIGNAL(triggered()), SLOT(showStatistics()));
+}
+
+void MainWindow::createSettingsActions()
+{
+ dictionaryOptionsAct = new QAction(QIcon(":/images/dic-options.png"),
+ tr("&Dictionary options"), this);
+ dictionaryOptionsAct->setShortcut(QString("Ctrl+1"));
+ connect(dictionaryOptionsAct, SIGNAL(triggered()), SLOT(openDictionaryOptions()));
+
+ fontColorSettingsAct = new QAction(QIcon(":/images/font-style.png"),
+ tr("&Font and color settings"), this);
+ fontColorSettingsAct->setShortcut(QString("Ctrl+2"));
+ connect(fontColorSettingsAct, SIGNAL(triggered()), SLOT(openFontColorSettings()));
+
+ studySettingsAct = new QAction(QIcon(":/images/study-settings.png"),
+ tr("&Study settings"), this);
+ studySettingsAct->setShortcut(QString("Ctrl+3"));
+ connect(studySettingsAct, SIGNAL(triggered()), SLOT(openStudySettings()));
+}
+
+void MainWindow::createHelpActions()
+{
+ helpAct = new QAction(tr("Help"), this);
+ helpAct->setShortcut(QString("F1"));
+ connect(helpAct, SIGNAL(triggered()), SLOT(help()));
+
+ aboutAct = new QAction(QIcon(":/images/freshmemory.png"), tr("About"), this);
+ connect(aboutAct, SIGNAL(triggered()), SLOT(about()));
+}
+
+void MainWindow::initActions()
+{
+ saveAct->setEnabled(false);
+ saveAsAct->setEnabled(false);
+ saveCopyAct->setEnabled(false);
+ exportAct->setEnabled(false);
+ removeTabAct->setEnabled(false);
+
+ cutAct->setEnabled(false);
+ copyAct->setEnabled(false);
+ pasteAct->setEnabled(false);
+ addImageAct->setEnabled(false);
+ insertRecordsAct->setEnabled(false);
+ removeRecordsAct->setEnabled(false);
+ findAct->setEnabled(false);
+ findAgainAct->setEnabled(false);
+
+ statisticsAct->setEnabled(false);
+ wordDrillAct->setEnabled(false);
+ spacedRepetitionAct->setEnabled(false);
+
+ dictionaryOptionsAct->setEnabled(false);
+}
+
+void MainWindow::createDictContextMenuActions()
+{
+ contextMenuActions << copyAct;
+ contextMenuActions << cutAct;
+ contextMenuActions << pasteAct;
+ contextMenuActions << insertRecordsAct;
+ contextMenuActions << removeRecordsAct;
+}
+
+void MainWindow::createSelectionActions()
+{
+ selectionActions << copyAct;
+ selectionActions << cutAct;
+ selectionActions << removeRecordsAct;
+}
+
+void MainWindow::createMenus()
+{
+ createFileMenu();
+
+ editMenu = menuBar()->addMenu( tr("&Edit") );
+ editMenu->addAction( undoAct );
+ editMenu->addAction( redoAct );
+ editMenu->addSeparator();
+
+ editMenu->addAction(cutAct);
+ editMenu->addAction(copyAct);
+ editMenu->addAction(pasteAct);
+ editMenu->addSeparator();
+
+ editMenu->addAction(addImageAct);
+ editMenu->addAction(insertRecordsAct);
+ editMenu->addAction(removeRecordsAct);
+ editMenu->addSeparator();
+ editMenu->addAction(findAct);
+ editMenu->addAction(findAgainAct);
+
+ viewMenu = menuBar()->addMenu( tr("&View") );
+
+ toolsMenu = menuBar()->addMenu( tr("&Tools") );
+ toolsMenu->addAction(wordDrillAct);
+ toolsMenu->addAction(spacedRepetitionAct);
+ toolsMenu->addAction(statisticsAct);
+
+ createOptionsMenu();
+
+ menuBar()->addSeparator();
+
+ helpMenu = menuBar()->addMenu( tr("&Help") );
+ helpMenu->addAction(helpAct);
+ helpMenu->addAction(aboutAct);
+}
+
+void MainWindow::createFileMenu()
+{
+ fileMenu = menuBar()->addMenu(tr("&File"));
+ fileMenu->addAction(newAct);
+ fileMenu->addAction(loadAct);
+ fileMenu->addAction(openFlashcardsAct);
+ fileMenu->addAction(saveAct);
+ fileMenu->addAction(saveAsAct);
+ fileMenu->addAction(saveCopyAct);
+ fileMenu->addAction(importAct);
+ fileMenu->addAction(exportAct);
+
+ createRecentFilesMenu();
+
+ fileMenu->addSeparator();
+ fileMenu->addAction(removeTabAct);
+ fileMenu->addAction(quitAct);
+}
+
+void MainWindow::createRecentFilesMenu()
+{
+ QMenu* recentFilesMenu = fileMenu->addMenu(tr("&Recent files"));
+ welcomeScreen->setRecentFilesMenu(recentFilesMenu);
+ recentFilesMan = new RecentFilesManager(this, recentFilesMenu);
+ connect(recentFilesMan, SIGNAL(recentFileTriggered(const QString&)),
+ SLOT(openFile(const QString&)));
+ connect(recentFilesMan, SIGNAL(addedFile()), welcomeScreen,
+ SLOT(updateRecentFilesButton()));
+}
+
+void MainWindow::createOptionsMenu()
+{
+ optionsMenu = menuBar()->addMenu(tr("&Options"));
+ optionsMenu->addAction( dictionaryOptionsAct );
+ optionsMenu->addAction( fontColorSettingsAct );
+ optionsMenu->addAction( studySettingsAct );
+
+ new LanguageMenu(optionsMenu);
+}
+
+void MainWindow::createToolBars()
+{
+ QToolBar* toolBar = addToolBar( tr("Main") );
+ toolBar->setObjectName("Main");
+ toolBar->addAction(newAct);
+ toolBar->addAction(loadAct);
+ toolBar->addAction(saveAct);
+ toolBar->addSeparator();
+ toolBar->addAction(findAct);
+ toolBar->addAction(wordDrillAct);
+ toolBar->addAction(spacedRepetitionAct);
+ toolBar->addAction(statisticsAct);
+ toolBar->addAction(addImageAct);
+}
+
+void MainWindow::createStatusBar()
+{
+ statusBar()->addPermanentWidget( totalRecordsLabel = new QLabel );
+ updateTotalRecordsLabel();
+}
+
+void MainWindow::createCentralWidget()
+ {
+ mainStackedWidget = new QStackedWidget;
+ welcomeScreen = new WelcomeScreen(this);
+ mainStackedWidget->addWidget(welcomeScreen);
+ mainStackedWidget->addWidget(createDictionaryView());
+ setCentralWidget(mainStackedWidget);
+ }
+
+QWidget* MainWindow::createDictionaryView()
+{
+ dictTabWidget = new DictionaryTabWidget(this);
+ connect(dictTabWidget, SIGNAL(currentChanged(int)), SLOT(curTabChanged(int)));
+ findPanel = new FindPanel(this);
+ findPanel->hide();
+
+ QVBoxLayout* dictionaryViewLt = new QVBoxLayout;
+ dictionaryViewLt->setContentsMargins(0, 0, 0, 0);
+ dictionaryViewLt->addWidget(dictTabWidget);
+ dictionaryViewLt->addWidget(findPanel);
+
+ QWidget* dictionaryView = new QWidget;
+ dictionaryView->setLayout(dictionaryViewLt);
+ return dictionaryView;
+}
+
+void MainWindow::curTabChanged(int tabIndex)
+{
+ updateMainStackedWidget(tabIndex);
+ model->setCurDictionary(tabIndex);
+ updateActions();
+ updateSelectionActions();
+ updateTotalRecordsLabel();
+ updatePackSelection(tabIndex);
+ updateCardPreview();
+}
+
+void MainWindow::updateMainStackedWidget(int tabIndex)
+{
+ if (tabIndex == -1)
+ mainStackedWidget->setCurrentIndex(0);
+ else
+ mainStackedWidget->setCurrentIndex(1);
+}
+
+void MainWindow::createDockWindows()
+ {
+ createPacksTreeDock();
+ createCardPreviewDock();
+ }
+
+void MainWindow::createPacksTreeDock()
+{
+ QDockWidget* packsDock = new QDockWidget(tr("Card packs"));
+ packsDock->setObjectName("Card-packs");
+ packsDock->setAllowedAreas(
+ Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+ packsTreeView = new QTreeView;
+ packsTreeView->setAllColumnsShowFocus(true);
+ packsTreeView->setRootIsDecorated(false);
+ packsTreeView->header()->setDefaultSectionSize(150);
+ PacksTreeModel* packsTreeModel = new PacksTreeModel(model);
+ packsTreeView->setModel(packsTreeModel);
+ packsDock->setWidget(packsTreeView);
+ addDockWidget(Qt::LeftDockWidgetArea, packsDock);
+ viewMenu->addAction(packsDock->toggleViewAction());
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(setCurDictTabByTreeIndex(const QModelIndex&)));
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateCardPreview()));
+ connect(packsTreeView, SIGNAL(activated(const QModelIndex&)),
+ SLOT(startSpacedRepetitionStudy()));
+}
+
+void MainWindow::createCardPreviewDock()
+{
+ cardPreview = new CardPreview(this);
+ addDockWidget(Qt::LeftDockWidgetArea, cardPreview);
+ viewMenu->addAction(cardPreview->toggleViewAction());
+}
+
+void MainWindow::updatePacksTreeView()
+{
+ static_cast<PacksTreeModel*>( packsTreeView->model() )->updateData();
+ packsTreeView->expandAll();
+ packsTreeView->resizeColumnToContents( 1 );
+ packsTreeView->resizeColumnToContents( 2 );
+ updatePackSelection();
+}
+
+void MainWindow::updateCardPreview()
+{
+ CardPack* pack = getCurCardPack();
+ Card* card = getCurCardFromPack(pack);
+ cardPreview->setContent(pack, card);
+}
+
+CardPack* MainWindow::getCurCardPack() const
+{
+ const Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return NULL;
+ int packId = packsTreeView->currentIndex().row();
+ return dict->cardPack(packId);
+}
+
+const DicRecord* MainWindow::getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const
+{
+ if(!dict || !dictView)
+ return NULL;
+ return dict->getRecord(dictView->currentIndex().row());
+}
+
+const DicRecord* MainWindow::getCurDictRecord() const
+{
+ return getCurDictRecord(model->curDictionary(), getCurDictView());
+}
+
+Card* MainWindow::getCurCardFromPack(CardPack* pack) const
+{
+ const DicRecord* record = getCurDictRecord();
+ if(!record)
+ return NULL;
+ return pack->getCard(record->getPreviewQuestionForPack(pack));
+}
+
+/// Proxy-view-aware copying
+void MainWindow::copyEntries()
+ {
+ const DictTableView* tableView = getCurDictView(); // May be proxy view
+ if( !tableView )
+ return;
+ QModelIndexList selectedIndexes = tableView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( tableView->model() );
+ if( proxyModel )
+ {
+ QModelIndexList srcIndexes;
+ foreach( QModelIndex index, selectedIndexes )
+ {
+ QModelIndex srcIndex = proxyModel->mapToSource( index );
+ srcIndexes << srcIndex;
+ }
+ selectedIndexes = srcIndexes;
+ }
+ Dictionary* dict = model->curDictionary();
+ QList<DicRecord*> entries;
+ foreach( QModelIndex index, selectedIndexes )
+ entries << const_cast<DicRecord*>(dict->getRecord( index.row() ));
+
+ CsvExportData params;
+ params.commentChar = '#';
+ params.fieldSeparators = "& ";
+ params.quoteAllFields = false;
+ params.textDelimiter = '"';
+ params.writeColumnNames = true;
+ DicCsvWriter csvWriter( entries );
+ QString copiedStr = csvWriter.toCsvString( params );
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText( copiedStr );
+ }
+
+void MainWindow::cutEntries()
+ {
+ copyEntries();
+ removeRecords();
+ }
+
+void MainWindow::pasteEntries()
+ {
+ // Get text from clipboard
+ QClipboard *clipboard = QApplication::clipboard();
+ QString pastedStr = clipboard->text();
+ if( pastedStr.isEmpty() )
+ return;
+
+ const DictTableView* dictView = getCurDictView(); // May be proxy view
+ Q_ASSERT( dictView );
+ DictTableModel* dictModel = dictView->dicTableModel(); // Always original model
+ Q_ASSERT( dictModel );
+
+ // Parse records to be pasted
+ DicCsvReader csvReader;
+ CsvImportData params;
+ params.firstLineIsHeader = true;
+ params.commentChar = '#';
+ params.fieldSeparationMode = EFieldSeparatorExactString;
+ params.fieldSeparators = "& ";
+ params.fromLine = 1;
+ params.textDelimiter = '"';
+ params.colsToImport = 0; // All
+ QList<DicRecord*> records = csvReader.readEntries( pastedStr, params );
+ if( records.empty() )
+ return;
+ QStringList pastedFieldNames = csvReader.fieldNames();
+
+ // Check for new pasted fields. Ask user what to do with them.
+ QStringList dicFieldNames = model->curDictionary()->fieldNames();
+ QStringList newFieldNames;
+ foreach( QString name, pastedFieldNames )
+ if( !dicFieldNames.contains( name ) )
+ newFieldNames << name;
+ if( !newFieldNames.isEmpty() )
+ {
+ QMessageBox msgBox;
+ msgBox.setWindowTitle( windowTitle() );
+ bool existingFields = pastedFieldNames.size() - newFieldNames.size() > 0;
+ msgBox.setText( tr("The pasted records contain %n new field(s)", "", newFieldNames.size()) + ":\n" + newFieldNames.join(", ") + "." );
+ msgBox.setInformativeText(tr("Do you want to add new fields to this dictionary?"));
+ msgBox.setIcon( QMessageBox::Question );
+ QPushButton* addFieldsBtn = msgBox.addButton( tr("Add new fields"), QMessageBox::YesRole );
+ QPushButton* pasteExistingBtn = msgBox.addButton( tr("Paste only existing fields"), QMessageBox::NoRole );
+ pasteExistingBtn->setEnabled( existingFields );
+ msgBox.addButton( QMessageBox::Cancel );
+ msgBox.setDefaultButton( addFieldsBtn );
+ int res = msgBox.exec();
+ QAbstractButton* clickedBtn = msgBox.clickedButton();
+ if( clickedBtn == addFieldsBtn )
+ dictModel->addFields( newFieldNames );
+ else if( res == QMessageBox::Cancel ) // do not paste records
+ return;
+ // If paste only existing fields, don't make any changes here, just continue.
+ }
+
+ QUndoCommand* command = new PasteRecordsCmd( records, this ); // Takes ownership of records
+ dictModel->undoStack()->push( command );
+ }
+
+void MainWindow::readSettings()
+{
+ loadGeneralSettings();
+ StudySettings::inst()->load();
+}
+
+void MainWindow::loadGeneralSettings()
+{
+ QSettings settings;
+ move(settings.value("main-pos", QPoint(100, 100)).toPoint());
+ resize(settings.value("main-size", QSize(600, 500)).toSize());
+ if (settings.value("main-maximized", false).toBool())
+ showMaximized();
+
+ restoreState(settings.value("main-state").toByteArray(), 0);
+ QStringList recentFiles = settings.value("main-recent-files").toStringList();
+ recentFilesMan->createRecentFileActs(recentFiles);
+ workPath = settings.value("work-path",
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).
+ toString();
+}
+
+void MainWindow::writeSettings()
+{
+ saveGeneralSettings();
+}
+
+void MainWindow::saveGeneralSettings()
+{
+ QSettings settings;
+ settings.setValue("main-maximized", isMaximized());
+ if(!isMaximized())
+ {
+ settings.setValue("main-pos", pos());
+ settings.setValue("main-size", size());
+ }
+ settings.setValue("main-state", saveState(0));
+ settings.setValue("main-recent-files", recentFilesMan->getFiles());
+ settings.setValue("work-path", workPath);
+ saveSession();
+}
+
+void MainWindow::saveSession()
+{
+ QSettings settings;
+ QStringList sessionFiles;
+ for(int i = 0; i < dictTabWidget->count(); i++)
+ {
+ const Dictionary* dict = model->dictionary(i);
+ sessionFiles << dict->getFilePath();
+ }
+ settings.setValue("session", sessionFiles);
+ settings.setValue("session-cur-tab", model->getCurDictIndex());
+}
+
+void MainWindow::startStudy(int studyType)
+ {
+ int packIx = packsTreeView->currentIndex().row();
+
+ if(studyWindow)
+ {
+ if((int)studyWindow->getStudyType() == studyType)
+ {
+ studyWindow->showNormal();
+ studyWindow->raise();
+ studyWindow->activateWindow();
+ return;
+ }
+ else
+ {
+ disconnect(studyWindow, SIGNAL(destroyed()), this, SLOT(activate()));
+ studyWindow->close();
+ }
+ }
+
+ IStudyModel* studyModel = model->createStudyModel(studyType, packIx);
+ if(!studyModel)
+ {
+ QMessageBox::critical(this, Strings::errorTitle(),
+ tr("The study cannot be started.", "First part of error message") + "\n" +
+ model->getErrorMessage());
+ return;
+ }
+
+ if( studyType == AppModel::SpacedRepetition )
+ connect(studyModel, SIGNAL(nextCardSelected()), SLOT(updatePacksTreeView()));
+
+ // Create study window
+ switch( studyType )
+ {
+ case AppModel::WordDrill:
+ studyWindow = new WordDrillWindow(dynamic_cast<WordDrillModel*>(studyModel), this );
+ break;
+ case AppModel::SpacedRepetition:
+ studyWindow = new SpacedRepetitionWindow(dynamic_cast<SpacedRepetitionModel*>(studyModel), this );
+ break;
+ }
+ connect(studyWindow, SIGNAL(destroyed()), SLOT(activate()));
+ studyWindow->show();
+ }
+
+void MainWindow::startSpacedRepetitionStudy()
+ {
+ startStudy( AppModel::SpacedRepetition );
+ }
+
+void MainWindow::goToDictionaryRecord(const Dictionary* aDictionary, int aRecordRow )
+ {
+ int dicIx = model->indexOfDictionary( const_cast<Dictionary*>( aDictionary ) );
+ if( dicIx < 0 )
+ return;
+ show();
+ raise();
+ activateWindow();
+ dictTabWidget->goToDictionaryRecord( dicIx, aRecordRow );
+ }
+
+void MainWindow::openDictionaryOptions()
+{
+ Dictionary* curDict = model->curDictionary();
+ if( !curDict )
+ return;
+ DictionaryOptionsDialog dicOptions( curDict, this );
+ int res = dicOptions.exec();
+ if (res == QDialog::Accepted)
+ {
+ curDict->setDictConfig( &dicOptions.m_dict );
+ getCurDictView()->dicTableModel()->resetData();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::openFontColorSettings()
+{
+ FontColorSettingsDialog dialog( this );
+ int res = dialog.exec();
+ if (res == QDialog::Accepted)
+ {
+ *(FieldStyleFactory::inst()) = *dialog.styleFactory();
+ FieldStyleFactory::inst()->save();
+ }
+}
+
+void MainWindow::openStudySettings()
+{
+ StudySettingsDialog dialog(this);
+ if(dialog.exec() == QDialog::Accepted)
+ {
+ *(StudySettings::inst()) = dialog.getSettings();
+ StudySettings::inst()->save();
+ }
+}
+
+void MainWindow::setCurDictTab(int index)
+{
+ dictTabWidget->setCurrentIndex(index);
+}
+
+void MainWindow::setCurDictTabByTreeIndex( const QModelIndex& aIndex )
+{
+ if( !aIndex.isValid() )
+ return;
+ dictTabWidget->setCurrentIndex( static_cast<TreeItem*>( aIndex.internalPointer() )->topParentRow() );
+}
+
+// Set the pack selection to the correct dictionary
+void MainWindow::updatePackSelection( int aDic )
+{
+ if( !model->curDictionary() )
+ return;
+ if( aDic == -1 ) // default value
+ aDic = model->curDictionary()->row();
+ QModelIndex curIx = packsTreeView->currentIndex();
+ if( curIx.isValid() && static_cast<TreeItem*>( curIx.internalPointer() )->topParentRow() == aDic )
+ return;
+ QAbstractItemModel* packsTreeModel = packsTreeView->model();
+ QModelIndex dicIx = packsTreeModel->index( aDic, 0, QModelIndex() );
+ int curPackIx;
+ if( !studyWindow.isNull() )
+ curPackIx = model->curCardPackIx();
+ else
+ curPackIx = 0; // first pack
+ QModelIndex packIndex = packsTreeModel->index( curPackIx, 0, dicIx );
+ packsTreeView->selectionModel()->setCurrentIndex( packIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
+}
+
+void MainWindow::showStatistics()
+{
+ StatisticsView(getCurDict()).exec();
+}
diff --git a/src/main-view/MainWindow.h b/src/main-view/MainWindow.h
new file mode 100644
index 0000000..5165118
--- /dev/null
+++ b/src/main-view/MainWindow.h
@@ -0,0 +1,218 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QtWidgets>
+
+class AppModel;
+class DictTableView;
+class Dictionary;
+class IStudyWindow;
+class FindPanel;
+class CardPreview;
+class RecentFilesManager;
+class DicRecord;
+class Card;
+class CardPack;
+class WelcomeScreen;
+
+#include "UndoCommands.h"
+#include "DictionaryTabWidget.h"
+
+class MainWindow: public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(AppModel* model);
+ ~MainWindow();
+
+ const DictTableView* getCurDictView() const;
+ QList<QAction*> getContextMenuActions() const { return contextMenuActions; }
+
+ void showContinueSearch() { dictTabWidget->showContinueSearch(); }
+ void goToDictionaryRecord( const Dictionary* aDictionary, int aRecordRow );
+
+public slots:
+ void openFile(const QString& filePath);
+
+private:
+ void init();
+ void addDictTab(Dictionary* dict);
+
+ void createActions();
+ void createFileActions();
+ void createEditActions();
+ void createAddImageAct();
+ void createinsertRecordsAct();
+ void createInsertRecordsAfterAct();
+ void createRemoveRecordsAct();
+ void createDictContextMenuActions();
+ void createSelectionActions();
+ void createToolsActions();
+ void createSettingsActions();
+ void createHelpActions();
+ void initActions();
+
+ void createMenus();
+ void createToolBars();
+ void createStatusBar();
+
+ void createCentralWidget();
+ void updateMainStackedWidget(int tabIndex);
+ QWidget* createDictionaryView();
+
+ void createDockWindows();
+ void createPacksTreeDock();
+ void createCardPreviewDock();
+
+ void readSettings();
+ void writeSettings();
+ bool doSave( const QString& aFilePath, bool aChangeFilePath = true );
+ bool proposeToSave();
+ void openSession();
+ void closeEvent(QCloseEvent *event);
+ void saveGeneralSettings();
+ void saveSession();
+ void loadGeneralSettings();
+
+ void updatePackSelection( int aDic = -1 );
+ Dictionary* getCurDict();
+ QString selectAddImageFile();
+ void checkAddImagePath();
+ QString copyImageFileToImagesDir(const QString& filePath);
+ QString createNewImageFilePath(const QString& dicImagesDir, const QString& filePath);
+ QString createImagesDirFilePath(const QString& dicImagesDir, const QString& filePath,
+ int suffixNum);
+ int getCurEditorCursorPos();
+ int getCurRow();
+ int getCurColumn();
+ void insertImageIntoCurEditor(int cursorPos, const QString& filePath);
+
+ void createFileMenu();
+ void createOptionsMenu();
+ void createRecentFilesMenu();
+
+ CardPack* getCurCardPack() const;
+ const DicRecord* getCurDictRecord() const;
+ const DicRecord* getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const;
+ Card* getCurCardFromPack(CardPack* pack) const;
+ bool isDictOpened(int index);
+ void setCurDictionary(int index);
+ void openFileWithDialog(const QString& dirPath);
+ bool loadFile(const QString& filePath);
+ void showOpenFileError();
+ void updateAfterOpenedDictionary(const QString& filePath);
+
+public slots:
+ void updateDictTab();
+ void updateActions();
+ void updatePasteAction();
+ void updateSelectionActions();
+ bool proposeToSave(int i);
+
+private slots:
+ void activate();
+ void curTabChanged(int tabIndex);
+ void updatePacksTreeView();
+ void updateCardPreview();
+ void updateAddImageAction();
+ void newFile();
+ void openFileWithDialog();
+ void openOnlineDictionaries();
+ bool Save();
+ bool saveStudy();
+ bool SaveAs( bool aChangeFilePath = true );
+ void SaveCopy();
+ void importFromCsv();
+ void exportToCsv();
+
+ void copyEntries();
+ void cutEntries();
+ void pasteEntries();
+ void addImage();
+ void insertRecords();
+ void removeRecords();
+ void pushToUnoStack(QUndoCommand* command);
+ void find();
+ void findAgain();
+ void openDictionaryOptions();
+ void openFontColorSettings();
+ void openStudySettings();
+
+ void startStudy(int aStudyType);
+ void startSpacedRepetitionStudy();
+ void help();
+ void about();
+ void updateTotalRecordsLabel();
+ void setCurDictTab(int index);
+ void setCurDictTabByTreeIndex(const QModelIndex& aIndex);
+ void showStatistics();
+
+ void saveStudyWithDelay(bool studyModified);
+
+private:
+ static const int AutoSaveStudyInterval = 3 * 60 * 1000; // ms
+
+private:
+ QString workPath;
+ QString addImagePath;
+ AppModel* model; // not own
+
+ // UI elements
+ QStackedWidget* mainStackedWidget;
+ DictionaryTabWidget* dictTabWidget;
+ WelcomeScreen* welcomeScreen;
+ QLabel* totalRecordsLabel;
+ QPointer<IStudyWindow> studyWindow;
+ FindPanel* findPanel;
+ QTreeView* packsTreeView;
+ CardPreview* cardPreview;
+
+ QMenu* fileMenu;
+ QMenu* editMenu;
+ QMenu* viewMenu;
+ QMenu* toolsMenu;
+ QMenu* optionsMenu;
+ QMenu* helpMenu;
+
+ RecentFilesManager* recentFilesMan;
+
+ // Actions
+ QAction* newAct;
+ QAction* loadAct;
+ QAction* openFlashcardsAct;
+ QAction* saveAct;
+ QAction* saveAsAct;
+ QAction* saveCopyAct;
+ QAction* importAct;
+ QAction* exportAct;
+ QAction* removeTabAct;
+ QAction* quitAct;
+
+ QAction* undoAct;
+ QAction* redoAct;
+ QAction* cutAct;
+ QAction* copyAct;
+ QAction* pasteAct;
+ QAction* addImageAct;
+ QAction* insertRecordsAct;
+ QAction* removeRecordsAct;
+ QAction* findAgainAct;
+ QAction* findAct;
+ QList<QAction*> contextMenuActions;
+ QList<QAction*> selectionActions;
+
+ QAction* wordDrillAct;
+ QAction* spacedRepetitionAct;
+ QAction* statisticsAct;
+
+ QAction* dictionaryOptionsAct;
+ QAction* fontColorSettingsAct;
+ QAction* studySettingsAct;
+
+ QAction* helpAct;
+ QAction* aboutAct;
+};
+
+#endif
diff --git a/src/main-view/PacksTreeModel.cpp b/src/main-view/PacksTreeModel.cpp
new file mode 100644
index 0000000..55b8c34
--- /dev/null
+++ b/src/main-view/PacksTreeModel.cpp
@@ -0,0 +1,93 @@
+#include "PacksTreeModel.h"
+#include "AppModel.h"
+#include "../dictionary/TreeItem.h"
+
+PacksTreeModel::PacksTreeModel( AppModel* aAppModel, QObject* aParent ):
+ QAbstractItemModel( aParent ), m_appModel( aAppModel )
+ {
+ }
+
+QVariant PacksTreeModel::data(const QModelIndex &index, int role) const
+ {
+ if( !index.isValid() )
+ return QVariant();
+ if( role != Qt::DisplayRole )
+ return QVariant();
+ TreeItem* item = static_cast<TreeItem*>( index.internalPointer() );
+ return item->data( index.column() );
+ }
+
+Qt::ItemFlags PacksTreeModel::flags(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return 0;
+ if( !index.parent().isValid() ) // First level item = dictionary
+ return Qt::ItemIsEnabled;
+ else // pack
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ }
+
+QVariant PacksTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
+ {
+ if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
+ switch( section )
+ {
+ case 0: return tr("Card pack"); break;
+ case 1: return tr("Sched"); break;
+ case 2: return tr("New"); break;
+ }
+ return QVariant();
+ }
+
+QModelIndex PacksTreeModel::index(int row, int column, const QModelIndex &parent) const
+ {
+ if( !hasIndex(row, column, parent) )
+ return QModelIndex();
+ if( !parent.isValid() ) // First level item = dictionary
+ {
+ Dictionary* dic = m_appModel->dictionary( row );
+ if( dic )
+ return createIndex( row, column, dic );
+ else
+ return QModelIndex();
+ }
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ const TreeItem* childItem = parentItem->child( row );
+ if( childItem )
+ return createIndex( row, column, (void*)childItem );
+ else
+ return QModelIndex();
+ }
+
+QModelIndex PacksTreeModel::parent(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return QModelIndex();
+ TreeItem* childItem = static_cast<TreeItem*>( index.internalPointer() );
+ if( !childItem )
+ return QModelIndex();
+ const TreeItem* parentItem = childItem->parent();
+
+ if( parentItem )
+ return createIndex( parentItem->row(), 0, (void*)parentItem );
+ else
+ return QModelIndex(); // The root item
+ }
+
+int PacksTreeModel::rowCount(const QModelIndex &parent) const
+ {
+ if( parent.column() > 0 ) // Only the first column may have children
+ return 0;
+ if( !parent.isValid() ) // Root item
+ return m_appModel->dictionariesNum();
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ return parentItem->childCount();
+ }
+
+void PacksTreeModel::updateData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+
diff --git a/src/main-view/PacksTreeModel.h b/src/main-view/PacksTreeModel.h
new file mode 100644
index 0000000..f04036d
--- /dev/null
+++ b/src/main-view/PacksTreeModel.h
@@ -0,0 +1,32 @@
+#ifndef PACKSTREEMODEL_H
+#define PACKSTREEMODEL_H
+
+#include <QAbstractItemModel>
+
+#include "../dictionary/CardPack.h"
+
+class AppModel;
+
+class PacksTreeModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ PacksTreeModel( AppModel* aAppModel, QObject* aParent = 0);
+
+ QVariant data( const QModelIndex &index, int role ) const;
+ Qt::ItemFlags flags( const QModelIndex &index ) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+ QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const;
+ QModelIndex parent( const QModelIndex &index ) const;
+ int rowCount( const QModelIndex &parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex &/*parent*/ = QModelIndex() ) const { return 3; }
+
+public slots:
+ void updateData();
+
+private:
+ AppModel* m_appModel;
+};
+
+#endif // PACKSTREEMODEL_H
diff --git a/src/main-view/RecentFilesManager.cpp b/src/main-view/RecentFilesManager.cpp
new file mode 100644
index 0000000..d554eb4
--- /dev/null
+++ b/src/main-view/RecentFilesManager.cpp
@@ -0,0 +1,99 @@
+#include "RecentFilesManager.h"
+
+RecentFilesManager::RecentFilesManager(QObject* parent, QMenu* recentFilesMenu):
+ QObject(parent), recentFilesMenu(recentFilesMenu)
+{
+}
+
+void RecentFilesManager::createRecentFileActs(const QStringList& recentFiles)
+{
+ if(recentFiles.isEmpty())
+ {
+ recentFilesMenu->setEnabled(false);
+ return;
+ }
+ int recentFilesNum = recentFiles.size();
+ if(recentFilesNum > MaxRecentFiles)
+ recentFilesNum = MaxRecentFiles;
+ for(int i = 0; i < recentFilesNum; i++)
+ recentFilesMenu->addAction(createRecentFileAction(recentFiles[i]));
+ updateActionTexts();
+}
+
+QAction* RecentFilesManager::createRecentFileAction(const QString& filePath)
+{
+ QAction* action = new QAction(this);
+ action->setData(QDir::fromNativeSeparators(filePath));
+ connect(action, SIGNAL(triggered()), this, SLOT(triggerRecentFile()));
+ return action;
+}
+
+void RecentFilesManager::updateActionTexts()
+{
+ for(int i = 0; i < recentFilesMenu->actions().size(); i++)
+ {
+ QAction* action = recentFilesMenu->actions()[i];
+ QString filePath = getActionFile(action);
+ QString fileName = getShortFileName(filePath);
+ QString fileDir = QDir::toNativeSeparators(getShortDirPath(filePath));
+ QString text = QString("&%1 %2 (%3)").arg((i + 1) % 10)
+ .arg(fileName).arg(fileDir);
+ action->setText(text);
+ }
+}
+
+QString RecentFilesManager::getActionFile(QAction* action)
+{
+ return action->data().toString();
+}
+
+QString RecentFilesManager::getLastUsedFilePath() const
+{
+ if(recentFilesMenu->actions().isEmpty())
+ return QString();
+ return getActionFile(recentFilesMenu->actions().first());
+}
+
+QString RecentFilesManager::getShortDirPath(const QString& filePath) const
+{
+ QString path = QFileInfo(filePath).absolutePath();
+ QFontMetrics metrics(recentFilesMenu->font());
+ return metrics.elidedText(path, Qt::ElideMiddle, MaxPathLength);
+}
+
+void RecentFilesManager::triggerRecentFile()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ if(action)
+ emit recentFileTriggered(getActionFile(action));
+}
+
+void RecentFilesManager::addFile( const QString& filePath)
+{
+ addRecentFileAction(filePath);
+ updateActionTexts();
+ emit addedFile();
+}
+
+void RecentFilesManager::addRecentFileAction(const QString& filePath)
+{
+ recentFilesMenu->setEnabled(true);
+ foreach(QAction* action, recentFilesMenu->actions())
+ if(action->data() == filePath)
+ recentFilesMenu->removeAction(action);
+ QAction* newAction = createRecentFileAction(filePath);
+ if(!recentFilesMenu->actions().isEmpty())
+ recentFilesMenu->insertAction(recentFilesMenu->actions().first(), newAction);
+ else
+ recentFilesMenu->addAction(newAction);
+ if(recentFilesMenu->actions().size() > MaxRecentFiles)
+ recentFilesMenu->removeAction(recentFilesMenu->actions().last());
+}
+
+QStringList RecentFilesManager::getFiles() const
+{
+ QStringList list;
+ foreach(QAction* action, recentFilesMenu->actions())
+ list << getActionFile(action);
+ return list;
+}
diff --git a/src/main-view/RecentFilesManager.h b/src/main-view/RecentFilesManager.h
new file mode 100644
index 0000000..7988a0e
--- /dev/null
+++ b/src/main-view/RecentFilesManager.h
@@ -0,0 +1,43 @@
+#ifndef RECENT_FILES_MANAGER_H
+#define RECENT_FILES_MANAGER_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class RecentFilesManager: public QObject
+{
+ Q_OBJECT
+
+public:
+ RecentFilesManager(QObject* parent, QMenu* recentFilesMenu);
+ void createRecentFileActs(const QStringList& recentFiles);
+ void addFile( const QString& filePath);
+ QString getLastUsedFilePath() const;
+ QStringList getFiles() const;
+
+private:
+ static QString getShortFileName(const QString& filePath)
+ { return QFileInfo(filePath).fileName(); }
+ static QString getActionFile(QAction* action);
+
+private:
+ QAction* createRecentFileAction(const QString& filePath);
+ void updateActionTexts();
+ QString getShortDirPath(const QString& filePath) const;
+ void addRecentFileAction(const QString& filePath);
+
+private slots:
+ void triggerRecentFile();
+
+signals:
+ void recentFileTriggered(const QString& filePath);
+ void addedFile();
+
+private:
+ static const int MaxRecentFiles = 10;
+ static const int MaxPathLength = 300;
+
+private:
+ QMenu* recentFilesMenu;
+};
+#endif
diff --git a/src/main-view/RecordEditor.cpp b/src/main-view/RecordEditor.cpp
new file mode 100644
index 0000000..3f7a96c
--- /dev/null
+++ b/src/main-view/RecordEditor.cpp
@@ -0,0 +1,122 @@
+#include "RecordEditor.h"
+
+#include <QtDebug>
+
+const float RecordEditor::EditorWidthIncrease = 1.5;
+
+RecordEditor::RecordEditor(QWidget* parent, const QRect& cellRect) :
+ QTextEdit(parent), cellRect(cellRect),
+ enabledSizeUpdates(true)
+{
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ connect( this, SIGNAL(textChanged()), SLOT(updateEditor()) );
+}
+
+void RecordEditor::startDrawing()
+{
+ clear();
+ enabledSizeUpdates = false;
+}
+
+void RecordEditor::endDrawing()
+{
+ enabledSizeUpdates = true;
+ updateEditor();
+}
+
+void RecordEditor::drawText(const QString& text)
+{
+ textCursor().insertText(text);
+}
+
+void RecordEditor::drawImage(const QString& filePath)
+{
+ QTextImageFormat format;
+ format.setVerticalAlignment(QTextCharFormat::AlignBaseline);
+ format.setName(filePath);
+ format.setHeight(ThumbnailSize);
+ textCursor().insertImage(format);
+}
+
+void RecordEditor::insertImage(int cursorPos, const QString& filePath)
+{
+ moveCursor(cursorPos);
+ drawImage(filePath);
+}
+
+void RecordEditor::moveCursor(int cursorPos)
+{
+ QTextCursor cursor = textCursor();
+ cursor.setPosition(cursorPos);
+ setTextCursor(cursor);
+}
+
+QString RecordEditor::getText() const
+{
+ QString text;
+ QTextBlock block = document()->begin();
+ while(block.isValid())
+ {
+ for(QTextBlock::iterator it = block.begin(); !it.atEnd(); it++)
+ {
+ QTextCharFormat format = it.fragment().charFormat();
+ if(format.isImageFormat())
+ text += QString("<img src=\"") + format.toImageFormat().name() + "\">";
+ else
+ text += it.fragment().text();
+ }
+ block = block.next();
+ }
+ return text;
+}
+
+QSize RecordEditor::sizeHint() const
+{
+ int width = getEditorWidth();
+ return QSize(width, getEditorHeight());
+}
+
+int RecordEditor::getEditorWidth() const
+{
+ int horMargins = contentsMargins().left() * 2;
+ int textWidth = cellRect.width() - horMargins;
+ document()->setTextWidth(textWidth);
+ if(textWrapped() && textWidth < MinWidth)
+ {
+ textWidth = MinWidth;
+ document()->setTextWidth(textWidth);
+ }
+ return textWidth + horMargins;
+}
+
+bool RecordEditor::textWrapped() const
+{
+ return document()->size().height() > cellRect.height();
+}
+
+int RecordEditor::getEditorHeight() const
+{
+ return document()->size().height() + contentsMargins().top() * 2;
+}
+
+void RecordEditor::updateEditor()
+{
+ if(!enabledSizeUpdates)
+ return;
+ adjustSize();
+ updatePos();
+}
+
+void RecordEditor::updatePos()
+{
+ QRect editorRect = cellRect;
+ editorRect.setSize(size());
+ QSize viewportSize = qobject_cast<QWidget*>(parent())->size();
+ if(editorRect.right() > viewportSize.width())
+ editorRect.moveRight(viewportSize.width());
+ if(editorRect.bottom() > viewportSize.height())
+ editorRect.moveBottom(viewportSize.height());
+ move(editorRect.topLeft());
+}
diff --git a/src/main-view/RecordEditor.h b/src/main-view/RecordEditor.h
new file mode 100644
index 0000000..40b5dc5
--- /dev/null
+++ b/src/main-view/RecordEditor.h
@@ -0,0 +1,46 @@
+#ifndef RECORD_EDITOR_H
+#define RECORD_EDITOR_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class RecordEditor: public QTextEdit, public FieldContentPainter
+{
+Q_OBJECT
+
+private:
+ static const int ThumbnailSize = 25;
+ static const int MinWidth = 150;
+ static const int MaxWidth = 400;
+ static const float EditorWidthIncrease;
+
+public:
+ RecordEditor(QWidget* parent, const QRect& cellRect);
+ QRect getCellRect() const { return cellRect; }
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+ void insertImage(int cursorPos, const QString& filePath);
+ QString getText() const;
+
+protected:
+ QSize sizeHint() const;
+
+private:
+ int getEditorWidth() const;
+ int getEditorHeight() const;
+ void updatePos();
+ bool textWrapped() const;
+ void moveCursor(int cursorPos);
+
+public slots:
+ void updateEditor();
+
+private:
+ QRect cellRect;
+ bool enabledSizeUpdates;
+};
+#endif
+
diff --git a/src/main-view/UndoCommands.cpp b/src/main-view/UndoCommands.cpp
new file mode 100644
index 0000000..349b2a7
--- /dev/null
+++ b/src/main-view/UndoCommands.cpp
@@ -0,0 +1,322 @@
+#include "UndoCommands.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../main-view/MainWindow.h"
+#include "../main-view/CardFilterModel.h"
+
+UndoRecordCmd::UndoRecordCmd( const MainWindow* aMainWin ):
+ m_mainWindow( aMainWin )
+ {
+ }
+
+const DictTableView* UndoRecordCmd::getCurView()
+ {
+ if( !m_mainWindow )
+ return NULL;
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ return dictView;
+ }
+
+CardFilterModel* UndoRecordCmd::getProxyModel( const DictTableView* aCurDictView )
+ {
+ CardFilterModel* proxyModel = qobject_cast<CardFilterModel*>( aCurDictView->model() );
+ return proxyModel;
+ }
+
+void UndoRecordCmd::insertRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = getCurView();
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+
+ // Insert rows by positions in the direct order
+ foreach( int row, aRowNumbers )
+ {
+ if( proxyModel )
+ proxyModel->addFilterRow( row );
+ m_dictModel->insertRows( row, 1 );
+ }
+ }
+
+void UndoRecordCmd::setRecords( QMap<int, DicRecord*> aRecords )
+ {
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Init rows by positions in the direct order
+ foreach( int row, aRecords.keys() )
+ {
+ QModelIndex index = m_dictModel->index( row, 0 );
+ DicRecord* record = aRecords.value( row );
+ m_dictModel->setData( index, QVariant::fromValue( *record ), DictTableModel::DicRecordRole );
+ }
+ dict->enableRecordUpdates();
+ }
+
+void UndoRecordCmd::removeRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Remove records by ranges from back
+ QListIterator<int> i( aRowNumbers );
+ i.toBack();
+ while( i.hasPrevious() )
+ {
+ int pos = i.previous();
+ if( proxyModel )
+ proxyModel->removeFilterRow( pos );
+ m_dictModel->removeRows( pos, 1 );
+ }
+ dict->enableRecordUpdates();
+ }
+
+
+/** Selects rows in the list.
+ * Sets the first row in the list as current.
+ */
+void UndoRecordCmd::selectRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ int topRow = aRowNumbers.first();
+ if( topRow > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ topRow = m_dictModel->rowCount() - 1;
+ QModelIndex topIndex = m_dictModel->index( topRow, 0, QModelIndex() );
+ QItemSelectionModel* selectionModel = dictView->selectionModel();
+ selectionModel->clear();
+ foreach( int row, aRowNumbers )
+ {
+ if( row > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ row = m_dictModel->rowCount() - 1;
+ QModelIndex index = m_dictModel->index( row, 0, QModelIndex() );
+ if( proxyModel )
+ {
+ index = proxyModel->mapFromSource( index );
+ if( !index.isValid() )
+ continue;
+ }
+ selectionModel->setCurrentIndex( index, QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ }
+ if( proxyModel )
+ topIndex = proxyModel->mapFromSource( topIndex );
+ if( topIndex.isValid() )
+ selectionModel->setCurrentIndex( topIndex, QItemSelectionModel::Current );
+ }
+
+void UndoRecordCmd::selectRow( int aRow )
+ {
+ QList<int> list;
+ list << aRow;
+ selectRows( list );
+ }
+
+//==============================================================================================================
+
+InsertRecordsCmd::InsertRecordsCmd( const MainWindow* aMainWin):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // If no selection, insert 1 row in the end
+ int pos = m_dictModel->rowCount();
+ int rowsNum = 1;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ if( !selectedRows.isEmpty() )
+ // Insert number of rows equal to the selected rows
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ qSort( selectedRows ); // to find the top row
+ pos = selectedRows.first().row();
+ rowsNum = selectedRows.size();
+ }
+ for( int r = pos; r < pos + rowsNum; r++ )
+ m_rowNumbers << r;
+
+ updateText();
+ }
+
+void InsertRecordsCmd::updateText()
+ {
+ setText(QObject::tr("Insert %n record(s)", "Undo action of inserting records", m_rowNumbers.size() ));
+ }
+
+void InsertRecordsCmd::redo()
+ {
+ insertRows( m_rowNumbers );
+ selectRows( m_rowNumbers );
+ }
+
+void InsertRecordsCmd::undo()
+ {
+ removeRows( m_rowNumbers );
+ selectRow( m_rowNumbers.first() );
+ }
+
+bool InsertRecordsCmd::mergeWith( const QUndoCommand* command )
+ {
+ const InsertRecordsCmd* otherInsertCmd = static_cast<const InsertRecordsCmd*>( command );
+ if( !otherInsertCmd )
+ return false;
+ if( otherInsertCmd->m_dictModel != m_dictModel )
+ return false;
+
+ // Find where to insert the other row numbers
+ int otherFirstRow = otherInsertCmd->m_rowNumbers.first();
+ int otherRowsNum = otherInsertCmd->m_rowNumbers.size();
+ int i = 0;
+ for( i = 0; i < m_rowNumbers.size(); i++ )
+ if( otherFirstRow <= m_rowNumbers[i] )
+ break;
+ int insertPos = i;
+ // Increment this row numbers by the number of other rows, after the insertion point
+ for( int i = insertPos; i < m_rowNumbers.size(); i++ )
+ m_rowNumbers[i] += otherRowsNum;
+ // Do the insertion of other rows
+ for( int i = 0; i < otherRowsNum; i++ )
+ m_rowNumbers.insert( insertPos + i, otherInsertCmd->m_rowNumbers[i] );
+
+ updateText();
+ return true;
+ }
+
+//==============================================================================================================
+
+RemoveRecordsCmd::RemoveRecordsCmd( const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ int recordsNum = 0;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ foreach( QModelIndex index, selectedRows )
+ {
+ int row = index.row();
+ DicRecord record = index.data( DictTableModel::DicRecordRole ).value<DicRecord>();
+ Q_ASSERT( !index.data( DictTableModel::DicRecordRole ).isNull() );
+ m_records.insert( row, new DicRecord( record ) );
+ recordsNum++;
+ }
+ setText(QObject::tr( "Remove %n record(s)", "Undo action of removing records", recordsNum ));
+ }
+
+RemoveRecordsCmd::~RemoveRecordsCmd()
+ {
+ qDeleteAll( m_records );
+ }
+
+void RemoveRecordsCmd::redo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
+
+void RemoveRecordsCmd::undo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+//==============================================================================================================
+
+EditRecordCmd::EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue ):
+ m_dictModel( aTableModel ), m_insertedRow( -1 )
+ {
+ m_row = aIndex.row();
+ m_col = aIndex.column();
+ m_oldStr = aIndex.data( Qt::EditRole ).toString();
+ m_newStr = aValue.toString();
+ if( m_row == m_dictModel->rowCount() - 1 && m_col == m_dictModel->columnCount() - 1 )
+ m_insertedRow = m_dictModel->rowCount();
+ QString editStr = m_newStr;
+ if( editStr.length() > MaxMenuEditStrLen )
+ editStr = editStr.left( MaxMenuEditStrLen ) + QChar( 8230 );
+ setText( QObject::tr( "Edit \"%1\"", "Undo action of editing a record").arg( editStr ) );
+ }
+
+void EditRecordCmd::redo()
+ {
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_newStr, Qt::EditRole );
+ if( m_insertedRow > -1 )
+ m_dictModel->insertRows( m_insertedRow, 1 );
+ }
+
+void EditRecordCmd::undo()
+ {
+ if( m_insertedRow > -1 )
+ m_dictModel->removeRows( m_insertedRow, 1 );
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_oldStr, Qt::EditRole );
+ }
+
+//==============================================================================================================
+
+PasteRecordsCmd::PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // Find position where to insert records
+ // Paste above selection. If no selection -> paste in the end of dictionary
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ int pastePos = m_dictModel->rowCount();
+ if( !selectedRows.isEmpty() )
+ {
+ qSort( selectedRows );
+ QModelIndex posIndex = selectedRows.first();
+
+ // If proxy view, convert position to source index
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ posIndex = proxyModel->mapToSource( posIndex );
+
+ pastePos = posIndex.row();
+ }
+ for( int i = 0; i < aRecords.size(); i++ )
+ m_records.insert( pastePos + i, aRecords.value( i ) );
+
+ setText(QObject::tr("Paste %n record(s)", "Undo action of pasting records", m_records.size() ));
+ }
+
+PasteRecordsCmd::~PasteRecordsCmd()
+ {
+ qDeleteAll( m_records.values() );
+ }
+
+void PasteRecordsCmd::redo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+void PasteRecordsCmd::undo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
diff --git a/src/main-view/UndoCommands.h b/src/main-view/UndoCommands.h
new file mode 100644
index 0000000..a4affec
--- /dev/null
+++ b/src/main-view/UndoCommands.h
@@ -0,0 +1,109 @@
+#ifndef UNDOCOMMANDS_H
+#define UNDOCOMMANDS_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class DicRecord;
+class MainWindow;
+class DictTableView;
+class DictTableModel;
+class CardFilterModel;
+
+class UndoRecordCmd: public QUndoCommand
+{
+public:
+ UndoRecordCmd( const MainWindow* aMainWin );
+
+protected:
+ const DictTableView* getCurView();
+ CardFilterModel* getProxyModel( const DictTableView* aCurDictView );
+
+ void insertRows( QList<int> aRowNumbers );
+ void setRecords( QMap<int, DicRecord*> aRecords );
+ void removeRows( QList<int> aRowNumbers );
+ void selectRows( QList<int> aRowNumbers );
+ void selectRow( int aRow );
+
+protected:
+ const MainWindow* m_mainWindow;
+ DictTableModel* m_dictModel;
+};
+
+class InsertRecordsCmd: public UndoRecordCmd
+{
+public:
+ InsertRecordsCmd(const MainWindow* aMainWin);
+
+ static const int Id = 1;
+
+ void updateText();
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* command );
+ int id() const { return Id; }
+
+private:
+ QList<int> m_rowNumbers;
+};
+
+class RemoveRecordsCmd: public UndoRecordCmd
+{
+public:
+ RemoveRecordsCmd( const MainWindow* aMainWin );
+ ~RemoveRecordsCmd();
+
+ static const int Id = 2;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+ // The first element is the top row
+};
+
+/// Doesn't inherit UndoRecordCmd!
+class EditRecordCmd: public QUndoCommand
+{
+public:
+ EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue );
+
+ static const int Id = 3;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ static const int MaxMenuEditStrLen = 20;
+
+ QAbstractItemModel* m_dictModel;
+ int m_row;
+ int m_col;
+ QString m_oldStr;
+ QString m_newStr;
+ int m_insertedRow;
+};
+
+class PasteRecordsCmd: public UndoRecordCmd
+{
+public:
+ PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin );
+ ~PasteRecordsCmd();
+
+ static const int Id = 4;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+};
+
+#endif // UNDOCOMMANDS_H
diff --git a/src/main-view/WelcomeScreen.cpp b/src/main-view/WelcomeScreen.cpp
new file mode 100644
index 0000000..806a57a
--- /dev/null
+++ b/src/main-view/WelcomeScreen.cpp
@@ -0,0 +1,51 @@
+#include "WelcomeScreen.h"
+
+WelcomeScreen::WelcomeScreen(QWidget* parent):
+ QWidget(parent)
+{
+ QPushButton* newButton = createWelcomeButton("filenew",
+ tr("Create new dictionary"), SLOT(newFile()));
+ QPushButton* openButton = createWelcomeButton("fileopen",
+ tr("Open existing dictionary"), SLOT(openFileWithDialog()));
+ QPushButton* openExampleButton = createWelcomeButton("",
+ tr("Open online dictionaries"), SLOT(openOnlineDictionaries()));
+ QPushButton* importButton = createWelcomeButton("",
+ tr("Import from CSV file"), SLOT(importFromCsv()));
+ recentFilesButton = createWelcomeButton("",
+ tr("Recent dictionaries"), NULL);
+
+ QVBoxLayout* buttonsLt = new QVBoxLayout;
+ buttonsLt->addWidget(newButton);
+ buttonsLt->addWidget(openButton);
+ buttonsLt->addWidget(openExampleButton);
+ buttonsLt->addWidget(importButton);
+ buttonsLt->addWidget(recentFilesButton);
+
+ QGridLayout* paddingLt = new QGridLayout;
+ paddingLt->addLayout(buttonsLt, 1, 1);
+ setLayout(paddingLt);
+}
+
+QPushButton* WelcomeScreen::createWelcomeButton(const QString& iconName,
+ const QString& text, const char* slot)
+{
+ static const QSize ButtonSize(350, 50);
+ QPushButton* button = new QPushButton(QIcon(QString(":/images/%1.png").
+ arg(iconName)), text);
+ button->setMaximumSize(ButtonSize);
+ if(slot)
+ connect(button, SIGNAL(clicked()), parentWidget(), slot);
+ return button;
+}
+
+void WelcomeScreen::updateRecentFilesButton()
+{
+ recentFilesButton->setEnabled(recentFilesButton->menu() &&
+ !recentFilesButton->menu()->actions().isEmpty());
+}
+
+void WelcomeScreen::setRecentFilesMenu(QMenu* menu)
+{
+ recentFilesButton->setMenu(menu);
+ updateRecentFilesButton();
+}
diff --git a/src/main-view/WelcomeScreen.h b/src/main-view/WelcomeScreen.h
new file mode 100644
index 0000000..17ca18e
--- /dev/null
+++ b/src/main-view/WelcomeScreen.h
@@ -0,0 +1,25 @@
+#ifndef WELCOME_SCREEN_H
+#define WELCOME_SCREEN_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class WelcomeScreen: public QWidget
+{
+ Q_OBJECT
+
+public:
+ WelcomeScreen(QWidget* parent);
+ void setRecentFilesMenu(QMenu* menu);
+
+public slots:
+ void updateRecentFilesButton();
+
+private:
+ QPushButton* createWelcomeButton(const QString& iconName, const QString& text,
+ const char* slot);
+
+private:
+ QPushButton* recentFilesButton;
+};
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..569c31a
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,102 @@
+#include <stdlib.h>
+#include <iostream>
+
+#include "main.h"
+#include "version.h"
+#include "strings.h"
+#include "main-view/AppModel.h"
+#include "main-view/MainWindow.h"
+
+int main(int argc, char *argv[])
+{
+ setlocale(LC_ALL, "en_US.UTF-8");
+ QApplication app(argc, argv);
+ initSettings();
+ setTranslations();
+
+ QString dictFilePath = "";
+ BuildStr = Strings::tr(Strings::s_build) + ": " + BUILD_REVISION;
+
+ if(processParams(dictFilePath))
+ return 0;
+
+ Q_INIT_RESOURCE(application);
+ AppModel model;
+ MainWindow mainWin(&model);
+ mainWin.show();
+ if(!dictFilePath.isEmpty())
+ mainWin.openFile(dictFilePath);
+ return app.exec();
+}
+
+void initSettings()
+{
+ QSettings::setDefaultFormat(QSettings::IniFormat);
+ QCoreApplication::setOrganizationName("freshmemory");
+ QCoreApplication::setApplicationName("freshmemory");
+}
+
+void setTranslations()
+{
+ QString locale = QSettings().value("lang").toString();
+ if(locale.isEmpty())
+ locale = QLocale::system().name();
+ installTranslator("qt_" + locale, QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+ installTranslator("freshmemory_" + locale, getResourcePath() + "/tr");
+}
+
+void installTranslator(const QString& fileName, const QString& path)
+{
+ QTranslator* translator = new QTranslator;
+ translator->load(fileName, path);
+ qApp->installTranslator(translator);
+}
+
+QString getResourcePath()
+{
+#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
+ return "/usr/share/freshmemory";
+#else
+ return QCoreApplication::applicationDirPath();
+#endif
+}
+
+bool processParams(QString& dictFilePath)
+{
+ QStringList argList = QCoreApplication::arguments();
+ QListIterator<QString> argIter = argList;
+ argIter.next(); // The first argument is the program calling full name
+ while( argIter.hasNext() )
+ {
+ QString argStr = argIter.next();
+ if( argStr == "--version" || argStr == "-v" )
+ {
+ QString formattedBuildStr;
+ if( !BuildStr.isEmpty() )
+ formattedBuildStr = BuildStr + "\n";
+
+ std::cout << (Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "\n" +
+ formattedBuildStr +
+ Strings::tr(Strings::s_author) + "\n" +
+ MainWindow::tr("Website:") + " http://fresh-memory.com\n").toStdString();
+ return true;
+ }
+ else if( argStr == "--help" || argStr == "-h" )
+ {
+ std::cout <<
+ (MainWindow::tr("Usage:") + " freshmemory [OPTIONS] [FILE]\n" +
+ MainWindow::tr("FILE is a dictionary filename to load.") +"\n\n" +
+ MainWindow::tr("Options:") + "\n" +
+ "\t-h, --help\t" + MainWindow::tr("Display this help and exit") + "\n" +
+ "\t-v, --version\t" + MainWindow::tr("Output version information and exit") + "\n")
+ .toStdString();
+ return true;
+ }
+ else if( !argIter.hasNext() ) // the last argument
+ {
+ dictFilePath = argStr;
+ return false;
+ }
+ }
+ return false;
+}
diff --git a/src/main.h b/src/main.h
new file mode 100644
index 0000000..b4922c3
--- /dev/null
+++ b/src/main.h
@@ -0,0 +1,8 @@
+#include <QtCore>
+#include <QtWidgets>
+
+void initSettings();
+void setTranslations();
+void installTranslator(const QString& fileName, const QString& path);
+QString getResourcePath();
+bool processParams(QString& dictFilePath);
diff --git a/src/settings/ColorBox.cpp b/src/settings/ColorBox.cpp
new file mode 100644
index 0000000..4ed73ab
--- /dev/null
+++ b/src/settings/ColorBox.cpp
@@ -0,0 +1,36 @@
+#include "ColorBox.h"
+
+ColorBox::ColorBox( QColor aColor )
+ {
+ setAutoFillBackground(true);
+ setFrameStyle( QFrame::Box );
+ setLineWidth(2);
+ setMinimumHeight( MinHeight );
+ setMinimumWidth( MinWidth );
+ setColor( aColor );
+ }
+
+void ColorBox::setColor( QColor aColor )
+ {
+ m_color = aColor;
+ if( isEnabled() )
+ setPalette( m_color );
+ else
+ setPalette( Qt::lightGray );
+ }
+
+void ColorBox::mousePressEvent(QMouseEvent* /*event*/)
+ {
+ QColor c = QColorDialog::getColor( color(), this );
+ if( c.isValid() )
+ {
+ setColor( c );
+ emit colorChanged( c );
+ }
+ }
+
+void ColorBox::changeEvent ( QEvent* event )
+ {
+ if( event->type() == QEvent::EnabledChange )
+ setColor( m_color );
+ }
diff --git a/src/settings/ColorBox.h b/src/settings/ColorBox.h
new file mode 100644
index 0000000..5844610
--- /dev/null
+++ b/src/settings/ColorBox.h
@@ -0,0 +1,28 @@
+#ifndef COLOR_BOX_H
+#define COLOR_BOX_H
+
+#include <QtWidgets>
+
+class ColorBox: public QFrame
+{
+ Q_OBJECT
+
+public:
+ ColorBox( QColor aColor = Qt::white );
+ QColor color() const { return m_color; }
+ void setColor( QColor aColor );
+
+protected:
+ void mousePressEvent ( QMouseEvent* event );
+ void changeEvent ( QEvent* event );
+
+signals:
+ void colorChanged( QColor aColor );
+
+private:
+ static const int MinHeight = 25;
+ static const int MinWidth = 50;
+ QColor m_color;
+};
+
+#endif
diff --git a/src/settings/FontColorSettingsDialog.cpp b/src/settings/FontColorSettingsDialog.cpp
new file mode 100644
index 0000000..beb5f01
--- /dev/null
+++ b/src/settings/FontColorSettingsDialog.cpp
@@ -0,0 +1,300 @@
+#include "FontColorSettingsDialog.h"
+#include "ColorBox.h"
+#include "../dic-options/FieldsPreviewModel.h"
+#include "StylesListModel.h"
+#include "StylePreviewModel.h"
+
+#include <QtCore>
+
+FontColorSettingsDialog::FontColorSettingsDialog(QWidget *parent) :
+ QDialog(parent), m_curStyle( NULL )
+ {
+ initData();
+ QHBoxLayout* bgColorLt = createBgColorSelector();
+ QLabel* stylesLbl = createStylesList();
+ QVBoxLayout* styleLt = createStyleControls();
+ createKeywordBox( styleLt );
+ styleLt->addStretch();
+ QLabel* previewLbl = createStylePreview();
+
+ m_okCancelBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel |
+ QDialogButtonBox::RestoreDefaults, Qt::Horizontal );
+ connect( m_okCancelBox, SIGNAL(accepted()), this, SLOT(accept()) );
+ connect( m_okCancelBox, SIGNAL(rejected()), this, SLOT(reject()) );
+ connect( m_okCancelBox, SIGNAL(clicked(QAbstractButton*)), SLOT(dialogButtonClicked(QAbstractButton*)) );
+
+ QGridLayout* mainLt = new QGridLayout;
+ mainLt->addLayout( bgColorLt, 0, 0, 1, 2 );
+ mainLt->addWidget( stylesLbl, 1, 0 );
+ mainLt->addWidget( m_stylesListView, 2, 0 );
+ mainLt->addLayout( styleLt, 2, 1 );
+ mainLt->addWidget( previewLbl, 1, 2 );
+ mainLt->addWidget( m_stylesPreview, 2, 2 );
+ mainLt->addWidget( m_okCancelBox, 3, 0, 1, 3 );
+
+ setLayout( mainLt );
+ setWindowTitle( tr("Font & color settings") );
+
+ m_stylesListView->setCurrentIndex( m_stylesListView->model()->index(0,0) );
+ }
+
+FontColorSettingsDialog::~FontColorSettingsDialog()
+ {
+ delete m_styleFactory;
+ }
+
+QHBoxLayout* FontColorSettingsDialog::createBgColorSelector()
+ {
+ m_bgColorSelector = new ColorBox( m_styleFactory->cardBgColor );
+ connect( m_bgColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setBgColor(QColor)) );
+
+ QHBoxLayout* bgColorLt = new QHBoxLayout;
+ bgColorLt->addWidget( new QLabel(tr("Card background color:")) );
+ bgColorLt->addWidget( m_bgColorSelector );
+ bgColorLt->addStretch();
+
+ return bgColorLt;
+ }
+
+QLabel* FontColorSettingsDialog::createStylesList()
+ {
+ QLabel* stylesLbl = new QLabel("<b>"+tr("Field styles")+"</b>");
+
+ QStringList styleNames = FieldStyleFactory::inst()->getStyleNames();
+ StylesListModel* stylesListModel = new StylesListModel( styleNames );
+ m_stylesListView = new QListView;
+ m_stylesListView->setModel( stylesListModel );
+ m_stylesListView->setMaximumWidth( StyleListMaxWidth );
+
+ connect( m_stylesListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateStyleControls(const QModelIndex&)) );
+
+ return stylesLbl;
+ }
+
+QVBoxLayout* FontColorSettingsDialog::createStyleControls()
+ {
+ m_fontSelector = new QFontComboBox;
+ m_fontSelector->setMaximumWidth( 180 );
+ connect( m_fontSelector, SIGNAL(currentFontChanged(QFont)), SLOT(setFontFamily(QFont)) );
+
+ m_sizeSelector = new QSpinBox;
+ m_sizeSelector->setMaximumWidth( SizeSelectorMaxWidth );
+ m_sizeSelector->setMinimum( 1 );
+ m_sizeSelector->setMaximum( 40 );
+ connect( m_sizeSelector, SIGNAL(valueChanged(int)), SLOT(setFontSize(int)) );
+ QVBoxLayout* styleLt = new QVBoxLayout;
+
+
+ QVBoxLayout* familyLt = new QVBoxLayout;
+ familyLt->addWidget( new QLabel(tr("Font family:")) );
+ familyLt->addWidget( m_fontSelector );
+ QVBoxLayout* sizeLt = new QVBoxLayout;
+ sizeLt->addWidget( new QLabel(tr("Size:")) );
+ sizeLt->addWidget( m_sizeSelector );
+ QHBoxLayout* lt0 = new QHBoxLayout;
+ lt0->setContentsMargins( QMargins() );
+ lt0->addLayout( familyLt );
+ lt0->addLayout( sizeLt );
+ lt0->addStretch();
+
+ m_fontColorSelector = new ColorBox;
+ m_boldCB = new QCheckBox(tr("Bold"));
+ m_italicCB = new QCheckBox(tr("Italic"));
+ QHBoxLayout* lt1 = new QHBoxLayout;
+ lt1->addWidget( new QLabel(tr("Color:")) );
+ lt1->addWidget( m_fontColorSelector );
+ lt1->addWidget( m_boldCB );
+ lt1->addWidget( m_italicCB );
+ lt1->addStretch();
+ connect( m_fontColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setStyleColor(QColor)) );
+ connect( m_boldCB, SIGNAL(stateChanged(int)), SLOT(setBoldFont(int)) );
+ connect( m_italicCB, SIGNAL(stateChanged(int)), SLOT(setItalicFont(int)) );
+
+ m_prefixEdit = new QLineEdit;
+ m_prefixEdit->setMaximumWidth( StyleEditMaxWidth );
+ m_suffixEdit = new QLineEdit;
+ m_suffixEdit->setMaximumWidth( StyleEditMaxWidth );
+ connect( m_prefixEdit, SIGNAL(textChanged(const QString&)), SLOT(setPrefix(const QString)) );
+ connect( m_suffixEdit, SIGNAL(textChanged(const QString&)), SLOT(setSuffix(const QString)) );
+
+ QHBoxLayout* lt2 = new QHBoxLayout;
+ lt2->addWidget( new QLabel(tr("Prefix:")) );
+ lt2->addWidget( m_prefixEdit );
+ lt2->addWidget( new QLabel(tr("Suffix:")) );
+ lt2->addWidget( m_suffixEdit );
+ lt2->addStretch();
+
+ styleLt->addLayout( lt0 );
+ styleLt->addLayout( lt1 );
+ styleLt->addLayout( lt2 );
+ return styleLt;
+ }
+
+void FontColorSettingsDialog::createKeywordBox( QVBoxLayout* aStyleLt )
+ {
+ m_keywordBox = new QGroupBox(tr("Keyword style"));
+ m_keywordBox->setCheckable(true);
+ connect( m_keywordBox, SIGNAL(toggled(bool)), SLOT(setKeywordStyle(bool)) );
+
+ m_keywordColorSelector = new ColorBox();
+ m_keywordBoldCB = new QCheckBox(tr("Bold"));
+ m_keywordItalicCB = new QCheckBox(tr("Italic"));
+ connect( m_keywordBoldCB, SIGNAL(stateChanged(int)), SLOT(setKeywordBoldFont(int)) );
+ connect( m_keywordItalicCB, SIGNAL(stateChanged(int)), SLOT(setKeywordItalicFont(int)) );
+ connect( m_keywordColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setKeywordColor(QColor)) );
+
+ QHBoxLayout* keywordLt1 = new QHBoxLayout;
+ keywordLt1->addWidget( new QLabel(tr("Color:")) );
+ keywordLt1->addWidget( m_keywordColorSelector );
+ keywordLt1->addWidget( m_keywordBoldCB );
+ keywordLt1->addWidget( m_keywordItalicCB );
+ keywordLt1->addStretch();
+
+ m_keywordBox->setLayout( keywordLt1 );
+ aStyleLt->addWidget( m_keywordBox );
+ }
+
+QLabel* FontColorSettingsDialog::createStylePreview()
+ {
+ QLabel* previewLbl = new QLabel("<b>"+tr("Style preview")+"</b>");
+ StylePreviewModel* stylesPreviewModel = new StylePreviewModel( this );
+ m_stylesPreview = new QTableView;
+ m_stylesPreview->setModel( stylesPreviewModel );
+ m_stylesPreview->verticalHeader()->hide();
+ m_stylesPreview->horizontalHeader()->hide();
+ m_stylesPreview->setShowGrid( false );
+ updatePreview();
+ return previewLbl;
+ }
+
+void FontColorSettingsDialog::initData()
+ {
+ m_styleFactory = new FieldStyleFactory( *FieldStyleFactory::inst() );
+ }
+
+void FontColorSettingsDialog::updateStyleControls( const QModelIndex& aIndex )
+{
+ QString styleName = m_styleFactory->getStyleNames().value( aIndex.row() );
+ m_curStyle = m_styleFactory->getStylePtr( styleName );
+
+ m_fontSelector->setCurrentFont( m_curStyle->font );
+ m_sizeSelector->setValue( m_curStyle->font.pointSize() );
+ m_boldCB->setChecked( m_curStyle->font.bold() );
+ m_italicCB->setChecked( m_curStyle->font.italic() );
+ m_fontColorSelector->setColor( m_curStyle->color );
+ m_prefixEdit->setText( m_curStyle->prefix );
+ m_suffixEdit->setText( m_curStyle->suffix );
+
+ // Keyword style
+ m_keywordBox->setChecked(m_curStyle->hasKeyword);
+ m_keywordBoldCB->setChecked( m_curStyle->keywordBold );
+ m_keywordItalicCB->setChecked( m_curStyle->keywordItalic );
+ m_keywordColorSelector->setColor( m_curStyle->keywordColor );
+}
+
+void FontColorSettingsDialog::updatePreview()
+ {
+ m_stylesPreview->reset();
+ m_stylesPreview->resizeColumnsToContents();
+ m_stylesPreview->resizeRowsToContents();
+ }
+
+void FontColorSettingsDialog::setBgColor( QColor aColor )
+ {
+ m_styleFactory->cardBgColor = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setFontFamily( QFont aFont )
+ {
+ m_curStyle->font.setFamily( aFont.family() );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setFontSize( int aSize )
+ {
+ m_curStyle->font.setPointSize( aSize );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setBoldFont( int aState )
+ {
+ m_curStyle->font.setBold( aState == Qt::Checked );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setItalicFont( int aState )
+ {
+ m_curStyle->font.setItalic( aState == Qt::Checked );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setStyleColor( QColor aColor )
+ {
+ m_curStyle->color = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setPrefix( const QString aText )
+ {
+ m_curStyle->prefix = aText;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setSuffix( const QString aText )
+ {
+ m_curStyle->suffix = aText;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordStyle( bool aNewKeywordStyleState )
+ {
+ m_curStyle->hasKeyword = aNewKeywordStyleState;
+ if(aNewKeywordStyleState)
+ {
+ m_keywordBoldCB->setChecked(m_curStyle->keywordBold);
+ m_keywordItalicCB->setChecked(m_curStyle->keywordItalic);
+ m_keywordColorSelector->setColor(m_curStyle->keywordColor);
+ }
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordBoldFont( int aState )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordBold = (aState == Qt::Checked);
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordItalicFont( int aState )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordItalic = (aState == Qt::Checked);
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordColor( QColor aColor )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordColor = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::dialogButtonClicked( QAbstractButton* aButton )
+ {
+ if( m_okCancelBox->buttonRole( aButton ) == QDialogButtonBox::ResetRole )
+ {
+ delete m_styleFactory;
+ m_styleFactory = new FieldStyleFactory;
+ m_bgColorSelector->setColor( m_styleFactory->cardBgColor );
+ QModelIndex index = m_stylesListView->model()->index(0,0);
+ m_stylesListView->setCurrentIndex( index );
+ updateStyleControls( index );
+ updatePreview();
+ }
+ }
diff --git a/src/settings/FontColorSettingsDialog.h b/src/settings/FontColorSettingsDialog.h
new file mode 100644
index 0000000..273f61c
--- /dev/null
+++ b/src/settings/FontColorSettingsDialog.h
@@ -0,0 +1,71 @@
+#ifndef FONTCOLORSETTINGSDIALOG_H
+#define FONTCOLORSETTINGSDIALOG_H
+
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QtWidgets>
+
+class ColorBox;
+
+class FontColorSettingsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ FontColorSettingsDialog(QWidget *parent = 0);
+ ~FontColorSettingsDialog();
+
+ const FieldStyleFactory* styleFactory() const { return m_styleFactory; }
+
+private:
+ void initData();
+ QHBoxLayout* createBgColorSelector();
+ QLabel* createStylesList();
+ QPushButton* createRestoreButton();
+ QVBoxLayout* createStyleControls();
+ void createKeywordBox( QVBoxLayout* aStyleLt );
+ QLabel* createStylePreview();
+ void updatePreview();
+
+private slots:
+ void updateStyleControls( const QModelIndex& aIndex );
+ void setBgColor(QColor aColor);
+ void setFontFamily(QFont aFont);
+ void setFontSize(int aSize);
+ void setBoldFont(int aState);
+ void setItalicFont(int aState);
+ void setStyleColor(QColor aColor);
+ void setPrefix(const QString aText);
+ void setSuffix(const QString aText);
+ void setKeywordStyle(bool aNewKeywordStyleState);
+ void setKeywordBoldFont(int aState);
+ void setKeywordItalicFont(int aState);
+ void setKeywordColor(QColor aColor);
+ void dialogButtonClicked( QAbstractButton* aButton );
+
+private:
+ static const int StyleListMaxWidth = 150;
+ static const int SizeSelectorMaxWidth = 50;
+ static const int StyleEditMaxWidth = 40;
+
+ FieldStyleFactory* m_styleFactory;
+ FieldStyle* m_curStyle;
+
+ ColorBox* m_bgColorSelector;
+ QListView* m_stylesListView;
+ QFontComboBox* m_fontSelector;
+ QSpinBox* m_sizeSelector;
+ QCheckBox* m_boldCB;
+ QCheckBox* m_italicCB;
+ ColorBox* m_fontColorSelector;
+ QLineEdit* m_prefixEdit;
+ QLineEdit* m_suffixEdit;
+ QGroupBox* m_keywordBox;
+ QCheckBox* m_keywordBoldCB;
+ QCheckBox* m_keywordItalicCB;
+ ColorBox* m_keywordColorSelector;
+ QTableView* m_stylesPreview;
+ QDialogButtonBox* m_okCancelBox;
+};
+
+#endif
diff --git a/src/settings/StudySettingsDialog.cpp b/src/settings/StudySettingsDialog.cpp
new file mode 100644
index 0000000..b38bf67
--- /dev/null
+++ b/src/settings/StudySettingsDialog.cpp
@@ -0,0 +1,141 @@
+#include "StudySettingsDialog.h"
+
+StudySettingsDialog::StudySettingsDialog(QWidget *parent):
+ QDialog(parent)
+ {
+ initData();
+ createUi();
+ updateControls();
+ }
+
+void StudySettingsDialog::initData()
+ {
+ settings = *StudySettings::inst();
+ }
+
+void StudySettingsDialog::createUi()
+{
+ createControls();
+ setLayout(createMainLayout());
+ setWindowTitle(tr("Study settings"));
+}
+
+void StudySettingsDialog::createControls()
+{
+ dayShiftBox = createSpinBox(0, 12);
+ newCardsShareBox = createSpinBox(1, 99);
+ randomnessBox = createSpinBox(1, 99);
+ cardsDayLimitBox = createSpinBox(1, 500);
+ newCardsDayLimitBox = createSpinBox(0, 200);
+ limitForAddingNewCardsBox = createSpinBox(20, 500);
+ showRandomlyCB = new QCheckBox(tr("Add new cards in random order"));
+ createButtonBox();
+}
+
+QSpinBox* StudySettingsDialog::createSpinBox(int min, int max)
+{
+ QSpinBox* spinBox = new QSpinBox;
+ spinBox->setMinimum(min);
+ spinBox->setMaximum(max);
+ return spinBox;
+}
+
+void StudySettingsDialog::createButtonBox()
+{
+ buttonBox = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel |
+ QDialogButtonBox::RestoreDefaults, Qt::Horizontal);
+ connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+ connect(buttonBox, SIGNAL(clicked(QAbstractButton*)),
+ SLOT(dialogButtonClicked(QAbstractButton*)));
+}
+
+QBoxLayout* StudySettingsDialog::createMainLayout()
+{
+ QHBoxLayout* upperPaddedLt = new QHBoxLayout;
+ upperPaddedLt->addLayout(createUpperLayout());
+ upperPaddedLt->addStretch();
+
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addLayout(upperPaddedLt);
+ mainLt->addWidget(showRandomlyCB);
+ mainLt->addWidget(createLimitsGroup());
+ mainLt->addWidget(buttonBox);
+ return mainLt;
+}
+
+QGridLayout* StudySettingsDialog::createUpperLayout()
+{
+ QGridLayout* layout = new QGridLayout;
+ int row = 0;
+ addUpperGridLine(layout, row++, tr("Day starts at, o'clock:"), dayShiftBox);
+ addUpperGridLine(layout, row++, tr("Share of new cards:"), newCardsShareBox, "%");
+ addUpperGridLine(layout, row++, tr("Repetition interval randomness:"), randomnessBox, "%");
+ return layout;
+}
+
+void StudySettingsDialog::addUpperGridLine(QGridLayout* layout, int row, const QString& label,
+ QWidget* widget, const QString& unitLabel)
+{
+ QLabel* labelWidget = new QLabel(label);
+ layout->addWidget(labelWidget, row, 0);
+ layout->addWidget(widget, row, 1);
+ layout->addWidget(new QLabel(unitLabel), row, 2);
+}
+
+QGroupBox* StudySettingsDialog::createLimitsGroup()
+{
+ QGridLayout* layout = new QGridLayout;
+ layout->setColumnStretch(0, 1);
+ int row = 0;
+ addLimitsGridLine(layout, row++, tr("Day reviews limit:"), cardsDayLimitBox);
+ addLimitsGridLine(layout, row++, tr("Day limit of new cards:"), newCardsDayLimitBox);
+ addLimitsGridLine(layout, row++,
+ tr("Don't add new cards after scheduled cards threshold:"),
+ limitForAddingNewCardsBox);
+ QGroupBox* group = new QGroupBox(tr("Limits"));
+ group->setLayout(layout);
+ return group;
+}
+
+void StudySettingsDialog::addLimitsGridLine(QGridLayout* layout, int row, const QString& label,
+ QWidget* widget)
+{
+ QLabel* labelWidget = new QLabel(label);
+ labelWidget->setWordWrap(true);
+ layout->addWidget(labelWidget, row, 0);
+ layout->addWidget(widget, row, 1);
+}
+
+const StudySettings StudySettingsDialog::getSettings()
+{
+ settings.showRandomly = showRandomlyCB->isChecked();
+ settings.newCardsShare = newCardsShareBox->value() / 100.;
+ settings.schedRandomness = randomnessBox->value() / 100.;
+ settings.cardsDayLimit = cardsDayLimitBox->value();
+ settings.newCardsDayLimit = newCardsDayLimitBox->value();
+ settings.limitForAddingNewCards = limitForAddingNewCardsBox->value();
+ settings.dayShift = dayShiftBox->value();
+ return settings;
+}
+
+void StudySettingsDialog::updateControls()
+{
+ showRandomlyCB->setChecked( settings.showRandomly );
+ newCardsShareBox->setValue( settings.newCardsShare * 100 );
+ randomnessBox->setValue( settings.schedRandomness * 100 );
+ cardsDayLimitBox->setValue( settings.cardsDayLimit );
+ newCardsDayLimitBox->setValue( settings.newCardsDayLimit );
+ limitForAddingNewCardsBox->setValue(settings.limitForAddingNewCards);
+ dayShiftBox->setValue( settings.dayShift );
+}
+
+void StudySettingsDialog::dialogButtonClicked( QAbstractButton* aButton )
+ {
+ if(buttonBox->buttonRole(aButton) == QDialogButtonBox::ResetRole)
+ {
+ settings = StudySettings();
+ updateControls();
+ }
+ }
diff --git a/src/settings/StudySettingsDialog.h b/src/settings/StudySettingsDialog.h
new file mode 100644
index 0000000..121767b
--- /dev/null
+++ b/src/settings/StudySettingsDialog.h
@@ -0,0 +1,45 @@
+#ifndef STUDYSETTINGSDIALOG_H
+#define STUDYSETTINGSDIALOG_H
+
+#include "../study/StudySettings.h"
+
+#include <QtWidgets>
+
+class StudySettingsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ StudySettingsDialog(QWidget *parent = 0);
+ const StudySettings getSettings();
+
+private:
+ void initData();
+ void createUi();
+ void updateControls();
+ void addUpperGridLine(QGridLayout* layout, int row, const QString& label, QWidget* widget,
+ const QString& unitLabel = "");
+ void addLimitsGridLine(QGridLayout* layout, int row, const QString& label, QWidget* widget);
+ QSpinBox* createSpinBox(int min, int max);
+ void createButtonBox();
+ void createControls();
+ QBoxLayout* createMainLayout();
+ QGridLayout*createUpperLayout();
+ QGroupBox* createLimitsGroup();
+
+private slots:
+ void dialogButtonClicked( QAbstractButton* aButton );
+
+private:
+ StudySettings settings;
+ QSpinBox* dayShiftBox;
+ QCheckBox* showRandomlyCB;
+ QSpinBox* newCardsShareBox;
+ QSpinBox* randomnessBox;
+ QSpinBox* cardsDayLimitBox;
+ QSpinBox* newCardsDayLimitBox;
+ QSpinBox* limitForAddingNewCardsBox;
+ QDialogButtonBox* buttonBox;
+};
+
+#endif
diff --git a/src/settings/StylePreviewModel.cpp b/src/settings/StylePreviewModel.cpp
new file mode 100644
index 0000000..e07c809
--- /dev/null
+++ b/src/settings/StylePreviewModel.cpp
@@ -0,0 +1,63 @@
+#include "StylePreviewModel.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QBrush>
+
+QVariant StylePreviewModel::data( const QModelIndex &index, int role ) const
+ {
+ if( !index.isValid())
+ return QVariant();
+ if( index.row() >= rowCount() || index.column() >= columnCount() )
+ return QVariant();
+
+ QString styleName = m_parent->styleFactory()->getStyleNames().value( index.row() );
+ FieldStyle fieldStyle = m_parent->styleFactory()->getStyle( styleName );
+ switch( index.column() )
+ {
+ case 0:
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return fieldStyle.prefix + styleName + fieldStyle.suffix;
+ case Qt::FontRole:
+ return fieldStyle.font;
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.color;
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ case 1:
+ if( fieldStyle.hasKeyword )
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return tr("keyword");
+ case Qt::FontRole:
+ return fieldStyle.getKeywordStyle().font;
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.keywordColor;
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ else
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return QVariant();
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
diff --git a/src/settings/StylePreviewModel.h b/src/settings/StylePreviewModel.h
new file mode 100644
index 0000000..1987431
--- /dev/null
+++ b/src/settings/StylePreviewModel.h
@@ -0,0 +1,32 @@
+#ifndef STYLESPREVIEWMODEL_H
+#define STYLEPREVIEWMODEL_H
+
+#include "../field-styles/FieldStyleFactory.h"
+#include "FontColorSettingsDialog.h"
+
+#include <QAbstractTableModel>
+
+class StylePreviewModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ StylePreviewModel( FontColorSettingsDialog* aParent ):
+ m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->styleFactory()->getStyleNames().size(); }
+ int columnCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return 2; }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ QVariant headerData( int /*section*/, Qt::Orientation /*orientation*/, int /*role = Qt::DisplayRole*/ ) const
+ { return QVariant(); }
+ Qt::ItemFlags flags(const QModelIndex &/*index*/) const {return Qt::NoItemFlags;}
+
+private:
+ FontColorSettingsDialog* m_parent;
+
+};
+
+#endif
diff --git a/src/settings/StylesListModel.h b/src/settings/StylesListModel.h
new file mode 100644
index 0000000..3899ba8
--- /dev/null
+++ b/src/settings/StylesListModel.h
@@ -0,0 +1,16 @@
+#ifndef STYLESLISTMODEL_H
+#define STYLESLISTMODEL_H
+
+#include <QStringListModel>
+
+class StylesListModel: public QStringListModel
+{
+ Q_OBJECT
+
+public:
+ StylesListModel( const QStringList& aStrings ): QStringListModel( aStrings) {}
+ Qt::ItemFlags flags( const QModelIndex& index ) const
+ { return QAbstractListModel::flags(index); }
+};
+
+#endif // STYLESLISTMODEL_H
diff --git a/src/statistics/BaseStatPage.cpp b/src/statistics/BaseStatPage.cpp
new file mode 100644
index 0000000..a7a8764
--- /dev/null
+++ b/src/statistics/BaseStatPage.cpp
@@ -0,0 +1,38 @@
+#include "BaseStatPage.h"
+#include "StatisticsParams.h"
+
+BaseStatPage::BaseStatPage(const StatisticsParams* statParams):
+ statParams(statParams)
+{
+}
+
+void BaseStatPage::init()
+{
+ createUi();
+ updateDataSet();
+}
+
+void BaseStatPage::createUi()
+{
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(createTitleLabel());
+ mainLt->addWidget(createChart());
+ mainLt->addWidget(createTotalReviewsLabel());
+ setLayout(mainLt);
+}
+
+QLabel* BaseStatPage::createTitleLabel()
+{
+ QLabel* titleLabel = new QLabel(getTitle());
+ titleLabel->setFont(QFont("Sans Serif", 18, QFont::Bold));
+ titleLabel->setAlignment(Qt::AlignCenter);
+ return titleLabel;
+}
+
+QWidget* BaseStatPage::createTotalReviewsLabel()
+{
+ totalReviewsLabel = new QLabel;
+ totalReviewsLabel->setFont(QFont("Sans Serif", 16));
+ totalReviewsLabel->setAlignment(Qt::AlignRight);
+ return totalReviewsLabel;
+}
diff --git a/src/statistics/BaseStatPage.h b/src/statistics/BaseStatPage.h
new file mode 100644
index 0000000..7ef91bd
--- /dev/null
+++ b/src/statistics/BaseStatPage.h
@@ -0,0 +1,31 @@
+#ifndef STATISTICS_PAGE_H
+#define STATISTICS_PAGE_H
+
+#include <QtWidgets>
+
+class StatisticsParams;
+
+class BaseStatPage: public QWidget
+{
+ Q_OBJECT
+public:
+ BaseStatPage(const StatisticsParams* statParams);
+ virtual void updateDataSet() = 0;
+ virtual QString getTitle() const = 0;
+ virtual bool usesTimePeriod() const = 0;
+
+protected:
+ void init();
+ virtual QWidget* createChart() = 0;
+
+private:
+ void createUi();
+ QLabel* createTitleLabel();
+ QWidget* createTotalReviewsLabel();
+
+protected:
+ const StatisticsParams* statParams;
+ QLabel* totalReviewsLabel;
+};
+
+#endif
diff --git a/src/statistics/ProgressPage.cpp b/src/statistics/ProgressPage.cpp
new file mode 100644
index 0000000..7676173
--- /dev/null
+++ b/src/statistics/ProgressPage.cpp
@@ -0,0 +1,41 @@
+#include "ProgressPage.h"
+#include "StatisticsParams.h"
+#include "../charts/PieChart.h"
+#include "../dictionary/CardPack.h"
+
+ProgressPage::ProgressPage(const StatisticsParams* statParams):
+ BaseStatPage(statParams)
+{
+ init();
+}
+
+QWidget* ProgressPage::createChart()
+{
+ chart = new PieChart;
+ chart->setColors({"#39c900", "#ece900", "#ff0000"});
+ return chart;
+}
+
+void ProgressPage::updateDataSet()
+{
+ updateCardsNumbers();
+ totalReviewsLabel->setText(tr("Total: %1").arg(allCardsNum));
+ chart->setDataSet(getDataSet());
+}
+
+void ProgressPage::updateCardsNumbers()
+{
+ const CardPack* pack = statParams->getCardPack();
+ allCardsNum = pack->cardsNum();
+ newCardsNum = pack->getNewCards().size();
+ toBeRepeated = pack->getActiveCards().size();
+}
+
+QList<DataPoint> ProgressPage::getDataSet() const
+{
+ QList<DataPoint> dataSet;
+ dataSet << DataPoint(tr("Studied"), allCardsNum - newCardsNum - toBeRepeated);
+ dataSet << DataPoint(tr("Scheduled for today"), toBeRepeated);
+ dataSet << DataPoint(tr("New"), newCardsNum);
+ return dataSet;
+}
diff --git a/src/statistics/ProgressPage.h b/src/statistics/ProgressPage.h
new file mode 100644
index 0000000..9adb4cc
--- /dev/null
+++ b/src/statistics/ProgressPage.h
@@ -0,0 +1,33 @@
+#ifndef PROGRESS_PAGE_H
+#define PROGRESS_PAGE_H
+
+#include <QtWidgets>
+#include "BaseStatPage.h"
+#include "../charts/DataPoint.h"
+
+class PieChart;
+
+class ProgressPage: public BaseStatPage
+{
+ Q_OBJECT
+public:
+ ProgressPage(const StatisticsParams* statParams);
+ void updateDataSet();
+ QString getTitle() const { return tr("Study progress"); }
+ bool usesTimePeriod() const {return false;}
+
+protected:
+ QWidget* createChart();
+
+private:
+ void updateCardsNumbers();
+ QList<DataPoint> getDataSet() const;
+
+private:
+ PieChart* chart;
+ int allCardsNum;
+ int newCardsNum;
+ int toBeRepeated;
+};
+
+#endif
diff --git a/src/statistics/ScheduledPage.cpp b/src/statistics/ScheduledPage.cpp
new file mode 100644
index 0000000..5e2b558
--- /dev/null
+++ b/src/statistics/ScheduledPage.cpp
@@ -0,0 +1,35 @@
+#include "ScheduledPage.h"
+#include "../dictionary/CardPack.h"
+
+ScheduledPage::ScheduledPage(const StatisticsParams* statParams):
+ TimeChartPage(statParams)
+{
+ init();
+}
+
+QList<QDateTime> ScheduledPage::getDates(const CardPack* pack) const
+{
+ QList<QDateTime> scheduled = pack->getScheduledDates();
+ adjustScheduledRecords(scheduled);
+ return scheduled;
+}
+
+void ScheduledPage::adjustScheduledRecords(QList<QDateTime>& scheduled)
+{
+ const QDate curDate = QDate::currentDate();
+ const QTime zeroTime = QTime(0, 0);
+ for(int i = 0; i < scheduled.size(); i++)
+ {
+ QDateTime& dateTime = scheduled[i];
+ if(dateTime.date() < curDate)
+ dateTime = QDateTime(curDate, zeroTime);
+ else if(laterThisDay(dateTime))
+ dateTime = QDateTime(curDate.addDays(1), zeroTime);
+ }
+}
+
+bool ScheduledPage::laterThisDay(QDateTime& dateTime)
+{
+ return dateTime.date() == QDate::currentDate() &&
+ dateTime > QDateTime::currentDateTime();
+}
diff --git a/src/statistics/ScheduledPage.h b/src/statistics/ScheduledPage.h
new file mode 100644
index 0000000..f5a4eec
--- /dev/null
+++ b/src/statistics/ScheduledPage.h
@@ -0,0 +1,22 @@
+#ifndef SCHEDULED_PAGE_H
+#define SCHEDULED_PAGE_H
+
+#include "TimeChartPage.h"
+
+class ScheduledPage: public TimeChartPage
+{
+ Q_OBJECT
+public:
+ ScheduledPage(const StatisticsParams* statParams);
+ QString getTitle() const { return tr("Scheduled cards"); }
+
+protected:
+ QList<QDateTime> getDates(const CardPack* pack) const;
+ int getDataDirection() const { return 1; }
+
+private:
+ static void adjustScheduledRecords(QList<QDateTime>& scheduled);
+ static bool laterThisDay(QDateTime& dateTime);
+};
+
+#endif
diff --git a/src/statistics/StatisticsParams.cpp b/src/statistics/StatisticsParams.cpp
new file mode 100644
index 0000000..4e5d80f
--- /dev/null
+++ b/src/statistics/StatisticsParams.cpp
@@ -0,0 +1,2 @@
+#include "StatisticsParams.h"
+#include "../dictionary/CardPack.h"
diff --git a/src/statistics/StatisticsParams.h b/src/statistics/StatisticsParams.h
new file mode 100644
index 0000000..9a805c6
--- /dev/null
+++ b/src/statistics/StatisticsParams.h
@@ -0,0 +1,20 @@
+#ifndef STATISTICS_PARAMS_H
+#define STATISTICS_PARAMS_H
+
+class CardPack;
+
+class StatisticsParams
+{
+public:
+ static const int Week = 7;
+
+public:
+ const CardPack* getCardPack() const { return cardPack; }
+ int getTimePeriod() const { return timePeriod; }
+
+protected:
+ const CardPack* cardPack;
+ int timePeriod;
+};
+
+#endif
diff --git a/src/statistics/StatisticsView.cpp b/src/statistics/StatisticsView.cpp
new file mode 100644
index 0000000..de1b16d
--- /dev/null
+++ b/src/statistics/StatisticsView.cpp
@@ -0,0 +1,182 @@
+#include "StatisticsView.h"
+#include "StudiedPage.h"
+#include "ScheduledPage.h"
+#include "ProgressPage.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/CardPack.h"
+
+const QSize StatisticsView::GridSize(150, 110);
+
+StatisticsView::StatisticsView(const Dictionary* dict):
+ dict(dict), periodLabel(NULL), periodBox(NULL)
+{
+ init();
+ createContentsList();
+ createPages();
+ createListItems();
+ createUi();
+ loadSettings();
+}
+
+void StatisticsView::init()
+{
+ cardPack = dict->cardPacks().first();
+ timePeriod = Week;
+ setWindowTitle(tr("Statistics") + " - " + dict->shortName());
+ setWindowIcon(QIcon(":/images/statistics.png"));
+}
+
+void StatisticsView::closeEvent(QCloseEvent* /*event*/)
+{
+ saveSettings();
+}
+
+void StatisticsView::createPages()
+{
+ pagesWidget = new QStackedWidget;
+ pagesWidget->addWidget(new ProgressPage(this));
+ pagesWidget->addWidget(new StudiedPage(this));
+ pagesWidget->addWidget(new ScheduledPage(this));
+}
+
+void StatisticsView::createContentsList()
+{
+ contentsWidget = new QListWidget;
+ contentsWidget->setViewMode(QListView::IconMode);
+ contentsWidget->setGridSize(GridSize);
+ contentsWidget->setIconSize(QSize(IconSize, IconSize));
+ contentsWidget->setMovement(QListView::Static);
+ contentsWidget->setFixedWidth(GridSize.width() + 4);
+}
+
+void StatisticsView::createListItems()
+{
+ QStringList icons = {"pie-chart-3d", "chart-past", "chart-future"};
+ for(int i = 0; i < icons.size(); i++)
+ new QListWidgetItem( QIcon(QString(":/images/%1.png").arg(icons[i])),
+ static_cast<BaseStatPage*>(pagesWidget->widget(i))->getTitle(),
+ contentsWidget);
+ connect(contentsWidget,
+ SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)),
+ SLOT(changePage(QListWidgetItem*, QListWidgetItem*)));
+ contentsWidget->setCurrentRow(0);
+}
+
+void StatisticsView::changePage(QListWidgetItem* curPage, QListWidgetItem* prevPage)
+{
+ if(!curPage)
+ curPage = prevPage;
+ pagesWidget->setCurrentIndex(contentsWidget->row(curPage));
+ updateChart();
+ updatePeriodBox();
+}
+
+void StatisticsView::createUi()
+{
+ QVBoxLayout* verLt = new QVBoxLayout;
+ verLt->addLayout(createControlLayout());
+ verLt->addWidget(pagesWidget);
+
+ QHBoxLayout* horLt = new QHBoxLayout;
+ horLt->addWidget(contentsWidget);
+ horLt->addLayout(verLt);
+ setLayout(horLt);
+}
+
+void StatisticsView::loadSettings()
+{
+ QSettings settings;
+ QVariant pointVar = settings.value("stats-pos");
+ if(pointVar.isNull())
+ return;
+ move(pointVar.toPoint());
+ resize(settings.value("stats-size").toSize());
+}
+
+void StatisticsView::saveSettings()
+{
+ QSettings settings;
+ settings.setValue("stats-pos", pos());
+ settings.setValue("stats-size", size());
+}
+
+QBoxLayout* StatisticsView::createControlLayout()
+{
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(new QLabel(tr("Card pack:")));
+ lt->addWidget(createPacksBox());
+ lt->addStretch();
+ periodLabel = new QLabel(tr("Period:"));
+ lt->addWidget(periodLabel);
+ lt->addWidget(createPeriodBox());
+ return lt;
+}
+
+QComboBox* StatisticsView::createPacksBox()
+{
+ QComboBox* packsBox = new QComboBox;
+ foreach(CardPack* pack, dict->cardPacks())
+ packsBox->addItem(pack->id());
+ connect(packsBox, SIGNAL(activated(int)), SLOT(setPack(int)));
+ return packsBox;
+}
+
+QComboBox* StatisticsView::createPeriodBox()
+{
+ periodBox = new QComboBox;
+ QPair<QString, int> period;
+ foreach(period, getPeriodsList())
+ periodBox->addItem(period.first, period.second);
+ periodBox->setMaxVisibleItems(12);
+ connect(periodBox, SIGNAL(activated(int)), SLOT(setPeriod(int)));
+ updatePeriodBox();
+ return periodBox;
+}
+
+QList<QPair<QString, int>> StatisticsView::getPeriodsList()
+{
+ static const int Week = 7;
+ static const int Month = 30;
+ static const int Year = 365;
+ return {
+ qMakePair(tr("%n week(s)", 0, 1), Week),
+ qMakePair(tr("%n week(s)", 0, 2), 2 * Week),
+ qMakePair(tr("%n week(s)", 0, 4), 4 * Week),
+ qMakePair(tr("%n month(s)", 0, 1), Month + 1),
+ qMakePair(tr("%n month(s)", 0, 2), 2 * Month + 1),
+ qMakePair(tr("%n month(s)", 0, 3), 3 * Month + 2),
+ qMakePair(tr("%n month(s)", 0, 6), 6 * Month + 3),
+ qMakePair(tr("%n year(s)", 0, 1), Year),
+ qMakePair(tr("%n year(s)", 0, 2), 2 * Year),
+ qMakePair(tr("%n year(s)", 0, 3), 3 * Year),
+ qMakePair(tr("All time"), -1)};
+}
+
+void StatisticsView::setPack(int packIndex)
+{
+ cardPack = dict->cardPacks().at(packIndex);
+ updateChart();
+}
+
+void StatisticsView::setPeriod(int index)
+{
+ timePeriod = periodBox->itemData(index).toInt();
+ updateChart();
+}
+
+void StatisticsView::updateChart()
+{
+ static_cast<BaseStatPage*>(pagesWidget->currentWidget())->updateDataSet();
+}
+
+void StatisticsView::updatePeriodBox()
+{
+ bool visiblePeriod = static_cast<BaseStatPage*>(
+ pagesWidget->currentWidget())->usesTimePeriod();
+ if(periodBox)
+ {
+ periodLabel->setVisible(visiblePeriod);
+ periodBox->setVisible(visiblePeriod);
+ }
+}
+
diff --git a/src/statistics/StatisticsView.h b/src/statistics/StatisticsView.h
new file mode 100644
index 0000000..560f8b8
--- /dev/null
+++ b/src/statistics/StatisticsView.h
@@ -0,0 +1,53 @@
+#ifndef STATISTICS_VIEW_H
+#define STATISTICS_VIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "StatisticsParams.h"
+
+class Dictionary;
+class CardPack;
+
+class StatisticsView: public QDialog, public StatisticsParams
+{
+ Q_OBJECT
+public:
+ StatisticsView(const Dictionary* dict);
+
+private:
+ static QList<QPair<QString, int>> getPeriodsList();
+
+private:
+ void init();
+ void closeEvent(QCloseEvent *event);
+ void createPages();
+ void createContentsList();
+ void createListItems();
+ void createUi();
+ void loadSettings();
+ void saveSettings();
+ QBoxLayout* createControlLayout();
+ QComboBox* createPacksBox();
+ QComboBox* createPeriodBox();
+ void updateChart();
+ void updatePeriodBox();
+
+private slots:
+ void changePage(QListWidgetItem* curPage, QListWidgetItem* prevPage);
+ void setPack(int packIndex);
+ void setPeriod(int index);
+
+private:
+ static const QSize GridSize;
+ static const int IconSize = 75;
+
+private:
+ const Dictionary* dict;
+ QListWidget* contentsWidget;
+ QLabel* periodLabel;
+ QComboBox* periodBox;
+ QStackedWidget* pagesWidget;
+};
+
+#endif
diff --git a/src/statistics/StudiedPage.cpp b/src/statistics/StudiedPage.cpp
new file mode 100644
index 0000000..5dbe83b
--- /dev/null
+++ b/src/statistics/StudiedPage.cpp
@@ -0,0 +1,17 @@
+#include "StudiedPage.h"
+#include "../dictionary/CardPack.h"
+#include "../study/StudyRecord.h"
+
+StudiedPage::StudiedPage(const StatisticsParams* statParams):
+ TimeChartPage(statParams)
+{
+ init();
+}
+
+QList<QDateTime> StudiedPage::getDates(const CardPack* pack) const
+{
+ QList<QDateTime> res;
+ foreach(StudyRecord record, pack->getStudyRecords())
+ res << record.date;
+ return res;
+}
diff --git a/src/statistics/StudiedPage.h b/src/statistics/StudiedPage.h
new file mode 100644
index 0000000..5ccb59b
--- /dev/null
+++ b/src/statistics/StudiedPage.h
@@ -0,0 +1,18 @@
+#ifndef STUDIED_PAGE_H
+#define STUDIED_PAGE_H
+
+#include "TimeChartPage.h"
+
+class StudiedPage: public TimeChartPage
+{
+ Q_OBJECT
+public:
+ StudiedPage(const StatisticsParams* statParams);
+ QString getTitle() const { return tr("Studied cards"); }
+
+protected:
+ QList<QDateTime> getDates(const CardPack* pack) const;
+ int getDataDirection() const { return -1; }
+};
+
+#endif
diff --git a/src/statistics/TimeChartPage.cpp b/src/statistics/TimeChartPage.cpp
new file mode 100644
index 0000000..62ca63b
--- /dev/null
+++ b/src/statistics/TimeChartPage.cpp
@@ -0,0 +1,54 @@
+#include "TimeChartPage.h"
+#include "StatisticsParams.h"
+#include "../charts/TimeChart.h"
+#include "../dictionary/CardPack.h"
+
+TimeChartPage::TimeChartPage(const StatisticsParams* statParams):
+ BaseStatPage(statParams)
+{
+}
+
+QWidget* TimeChartPage::createChart()
+{
+ chart = new TimeChart;
+ chart->setLabels(tr("Date"), tr("Cards"));
+ return chart;
+}
+
+void TimeChartPage::updateDataSet()
+{
+ QList<QDateTime> dates = getDates(statParams->getCardPack());
+ int period = statParams->getTimePeriod();
+ if(period == -1)
+ period = getStudyPeriodLength(dates);
+ int reviewsNum = getReviewsNum(dates, period);
+ totalReviewsLabel->setText(tr("Total: %1").arg(reviewsNum));
+ chart->setDates(dates, period, getDataDirection());
+}
+
+int TimeChartPage::getStudyPeriodLength(const QList<QDateTime>& dates) const
+{
+ const int minPeriod = 7;
+ QDateTime minDate = QDateTime::currentDateTime();
+ QDateTime maxDate = minDate;
+ foreach(QDateTime date, dates)
+ if(date < minDate)
+ minDate = date;
+ else if(date > maxDate)
+ maxDate = date;
+ int res = minDate.daysTo(maxDate) + 1;
+ if(res < minPeriod)
+ res = minPeriod;
+ return res;
+}
+
+int TimeChartPage::getReviewsNum(const QList<QDateTime>& dates,
+ int period) const
+{
+ QDateTime curDate = QDateTime::currentDateTime();
+ int res = 0;
+ foreach(QDateTime date, dates)
+ if(qAbs(date.daysTo(curDate)) < period)
+ res++;
+ return res;
+}
diff --git a/src/statistics/TimeChartPage.h b/src/statistics/TimeChartPage.h
new file mode 100644
index 0000000..5e48d4c
--- /dev/null
+++ b/src/statistics/TimeChartPage.h
@@ -0,0 +1,33 @@
+#ifndef TIME_CHART_PAGE_H
+#define TIME_CHART_PAGE_H
+
+#include <QtWidgets>
+#include "BaseStatPage.h"
+#include "../charts/DataPoint.h"
+
+class TimeChart;
+class StatisticsParams;
+class CardPack;
+
+class TimeChartPage: public BaseStatPage
+{
+ Q_OBJECT
+public:
+ TimeChartPage(const StatisticsParams* statParams);
+ void updateDataSet();
+ bool usesTimePeriod() const {return true;}
+
+protected:
+ virtual QList<QDateTime> getDates(const CardPack* pack) const = 0;
+ virtual int getDataDirection() const = 0;
+ QWidget* createChart();
+
+private:
+ int getStudyPeriodLength(const QList<QDateTime>& dates) const;
+ int getReviewsNum(const QList<QDateTime>& dates, int period) const;
+
+private:
+ TimeChart* chart;
+};
+
+#endif
diff --git a/src/strings.cpp b/src/strings.cpp
new file mode 100644
index 0000000..54d2fb9
--- /dev/null
+++ b/src/strings.cpp
@@ -0,0 +1,6 @@
+#include "strings.h"
+
+const char* Strings::s_build = QT_TRANSLATE_NOOP("Strings", "Build");
+const char* Strings::s_author = QT_TRANSLATE_NOOP("Strings", "Author: Mykhaylo Kopytonenko");
+const char* Strings::s_appTitle = QT_TRANSLATE_NOOP("Strings", "Fresh Memory");
+const char* Strings::s_error = QT_TRANSLATE_NOOP("Strings", "Error");
diff --git a/src/strings.h b/src/strings.h
new file mode 100644
index 0000000..bd7925a
--- /dev/null
+++ b/src/strings.h
@@ -0,0 +1,22 @@
+#ifndef STRINGS_H
+#define STRINGS_H
+
+#include <QObject>
+#include <QString>
+
+class Strings: public QObject
+{
+ Q_OBJECT
+
+public:
+ static const char* s_build;
+ static const char* s_author;
+ static const char* s_appTitle;
+ static const char* s_error;
+
+public:
+ static QString errorTitle() { return tr(s_appTitle) + " - " + tr(s_error); }
+
+};
+
+#endif
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
diff --git a/src/utils/IRandomGenerator.h b/src/utils/IRandomGenerator.h
new file mode 100644
index 0000000..08c6f95
--- /dev/null
+++ b/src/utils/IRandomGenerator.h
@@ -0,0 +1,17 @@
+#ifndef I_RANDOM_GENERATOR_H
+#define I_RANDOM_GENERATOR_H
+
+#include <QtCore>
+
+class IRandomGenerator
+{
+public:
+ virtual ~IRandomGenerator() {}
+ virtual double getInRange_11() const = 0;
+ virtual double getInRange_01() const = 0;
+ virtual int getRand() const = 0;
+ virtual int getRand(int maxNum) const = 0;
+ virtual QByteArray getArray() const = 0;
+};
+
+#endif
diff --git a/src/utils/RandomGenerator.cpp b/src/utils/RandomGenerator.cpp
new file mode 100644
index 0000000..9ffe4fb
--- /dev/null
+++ b/src/utils/RandomGenerator.cpp
@@ -0,0 +1,38 @@
+#include "RandomGenerator.h"
+
+#include <stdlib.h>
+#include <time.h>
+
+RandomGenerator::RandomGenerator()
+{
+ srand(time(NULL));
+}
+
+double RandomGenerator::getInRange_11() const
+{
+ return 2. * rand() / RAND_MAX - 1;
+}
+
+double RandomGenerator::getInRange_01() const
+{
+ return float(rand()) / RAND_MAX;
+}
+
+int RandomGenerator::getRand() const
+{
+ return rand();
+}
+
+int RandomGenerator::getRand(int maxNum) const
+{
+ return rand() % maxNum;
+}
+
+QByteArray RandomGenerator::getArray() const
+{
+ const int size = 16;
+ QByteArray res;
+ for(int i = 0; i < size; i++)
+ res += rand() % 256;
+ return res;
+}
diff --git a/src/utils/RandomGenerator.h b/src/utils/RandomGenerator.h
new file mode 100644
index 0000000..da36070
--- /dev/null
+++ b/src/utils/RandomGenerator.h
@@ -0,0 +1,16 @@
+#ifndef RANDOM_GENERATOR_H
+#define RANDOM_GENERATOR_H
+
+#include "IRandomGenerator.h"
+
+class RandomGenerator: public IRandomGenerator
+{
+public:
+ RandomGenerator();
+ double getInRange_11() const;
+ double getInRange_01() const;
+ int getRand() const;
+ int getRand(int maxNum) const;
+ QByteArray getArray() const;
+};
+#endif
diff --git a/src/utils/TimeProvider.cpp b/src/utils/TimeProvider.cpp
new file mode 100644
index 0000000..db24c5e
--- /dev/null
+++ b/src/utils/TimeProvider.cpp
@@ -0,0 +1,6 @@
+#include "TimeProvider.h"
+
+QDateTime TimeProvider::get()
+{
+ return QDateTime::currentDateTime();
+}
diff --git a/src/utils/TimeProvider.h b/src/utils/TimeProvider.h
new file mode 100644
index 0000000..697d8eb
--- /dev/null
+++ b/src/utils/TimeProvider.h
@@ -0,0 +1,12 @@
+#ifndef TIME_PROVIDER_H
+#define TIME_PROVIDER_H
+
+#include <QtCore>
+
+class TimeProvider
+{
+public:
+ static QDateTime get();
+};
+
+#endif
diff --git a/src/version.cpp b/src/version.cpp
new file mode 100644
index 0000000..4ee51bf
--- /dev/null
+++ b/src/version.cpp
@@ -0,0 +1,3 @@
+#include "version.h"
+
+QString BuildStr;
diff --git a/src/version.h b/src/version.h
new file mode 100644
index 0000000..526a65c
--- /dev/null
+++ b/src/version.h
@@ -0,0 +1,19 @@
+#ifndef VERSION_H
+#define VERSION_H
+
+#include <QString>
+
+#define DIC_VERSION "1.4" // Version of the dictionary XML file
+#define STUDY_VERSION "1.4" // Version of the study file
+
+#ifndef FM_VERSION
+ #define FM_VERSION "0.0"
+#endif
+
+#ifndef BUILD_REVISION
+ #define BUILD_REVISION ""
+#endif
+
+extern QString BuildStr;
+
+#endif