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 | 
