summaryrefslogtreecommitdiff
path: root/src/dictionary
diff options
context:
space:
mode:
authorJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
committerJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
commitd24f813f3f2a05c112e803e4256b53535895fc98 (patch)
tree601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9 /src/dictionary
Initial mirror commitHEADmaster
Diffstat (limited to 'src/dictionary')
-rw-r--r--src/dictionary/Card.cpp125
-rw-r--r--src/dictionary/Card.h50
-rw-r--r--src/dictionary/CardPack.cpp432
-rw-r--r--src/dictionary/CardPack.h144
-rw-r--r--src/dictionary/DicCsvReader.cpp205
-rw-r--r--src/dictionary/DicCsvReader.h41
-rw-r--r--src/dictionary/DicCsvWriter.cpp110
-rw-r--r--src/dictionary/DicCsvWriter.h31
-rw-r--r--src/dictionary/DicRecord.cpp62
-rw-r--r--src/dictionary/DicRecord.h40
-rw-r--r--src/dictionary/Dictionary.cpp601
-rw-r--r--src/dictionary/Dictionary.h187
-rw-r--r--src/dictionary/DictionaryReader.cpp384
-rw-r--r--src/dictionary/DictionaryReader.h44
-rw-r--r--src/dictionary/DictionaryWriter.cpp79
-rw-r--r--src/dictionary/DictionaryWriter.h26
-rw-r--r--src/dictionary/Field.cpp18
-rw-r--r--src/dictionary/Field.h46
-rw-r--r--src/dictionary/ICardPack.cpp23
-rw-r--r--src/dictionary/ICardPack.h33
-rw-r--r--src/dictionary/IDictionary.cpp59
-rw-r--r--src/dictionary/IDictionary.h51
-rw-r--r--src/dictionary/TreeItem.h25
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