diff options
Diffstat (limited to 'src/dictionary')
| -rw-r--r-- | src/dictionary/Card.cpp | 125 | ||||
| -rw-r--r-- | src/dictionary/Card.h | 50 | ||||
| -rw-r--r-- | src/dictionary/CardPack.cpp | 432 | ||||
| -rw-r--r-- | src/dictionary/CardPack.h | 144 | ||||
| -rw-r--r-- | src/dictionary/DicCsvReader.cpp | 205 | ||||
| -rw-r--r-- | src/dictionary/DicCsvReader.h | 41 | ||||
| -rw-r--r-- | src/dictionary/DicCsvWriter.cpp | 110 | ||||
| -rw-r--r-- | src/dictionary/DicCsvWriter.h | 31 | ||||
| -rw-r--r-- | src/dictionary/DicRecord.cpp | 62 | ||||
| -rw-r--r-- | src/dictionary/DicRecord.h | 40 | ||||
| -rw-r--r-- | src/dictionary/Dictionary.cpp | 601 | ||||
| -rw-r--r-- | src/dictionary/Dictionary.h | 187 | ||||
| -rw-r--r-- | src/dictionary/DictionaryReader.cpp | 384 | ||||
| -rw-r--r-- | src/dictionary/DictionaryReader.h | 44 | ||||
| -rw-r--r-- | src/dictionary/DictionaryWriter.cpp | 79 | ||||
| -rw-r--r-- | src/dictionary/DictionaryWriter.h | 26 | ||||
| -rw-r--r-- | src/dictionary/Field.cpp | 18 | ||||
| -rw-r--r-- | src/dictionary/Field.h | 46 | ||||
| -rw-r--r-- | src/dictionary/ICardPack.cpp | 23 | ||||
| -rw-r--r-- | src/dictionary/ICardPack.h | 33 | ||||
| -rw-r--r-- | src/dictionary/IDictionary.cpp | 59 | ||||
| -rw-r--r-- | src/dictionary/IDictionary.h | 51 | ||||
| -rw-r--r-- | src/dictionary/TreeItem.h | 25 | 
23 files changed, 2816 insertions, 0 deletions
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
  | 
