diff options
author | Jedidiah Barber <contact@jedbarber.id.au> | 2021-07-14 11:49:10 +1200 |
---|---|---|
committer | Jedidiah Barber <contact@jedbarber.id.au> | 2021-07-14 11:49:10 +1200 |
commit | d24f813f3f2a05c112e803e4256b53535895fc98 (patch) | |
tree | 601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9 /src |
Diffstat (limited to 'src')
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 © ..."), 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 += " "; + QString colorBoxPattern = "<p><span style=\"background-color: %1;\">" + + boxSpace + "</span> "; + 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 |