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
|