summaryrefslogtreecommitdiff
path: root/src/main-view
diff options
context:
space:
mode:
Diffstat (limited to 'src/main-view')
-rw-r--r--src/main-view/AboutDialog.cpp27
-rw-r--r--src/main-view/AboutDialog.h17
-rw-r--r--src/main-view/AppModel.cpp196
-rw-r--r--src/main-view/AppModel.h64
-rw-r--r--src/main-view/CardFilterModel.cpp24
-rw-r--r--src/main-view/CardFilterModel.h21
-rw-r--r--src/main-view/CardPreview.cpp55
-rw-r--r--src/main-view/CardPreview.h30
-rw-r--r--src/main-view/DictTableDelegate.cpp128
-rw-r--r--src/main-view/DictTableDelegate.h44
-rw-r--r--src/main-view/DictTableDelegatePainter.cpp61
-rw-r--r--src/main-view/DictTableDelegatePainter.h36
-rw-r--r--src/main-view/DictTableModel.cpp142
-rw-r--r--src/main-view/DictTableModel.h52
-rw-r--r--src/main-view/DictTableView.cpp84
-rw-r--r--src/main-view/DictTableView.h36
-rw-r--r--src/main-view/DictionaryTabWidget.cpp108
-rw-r--r--src/main-view/DictionaryTabWidget.h46
-rw-r--r--src/main-view/FieldContentCodec.cpp52
-rw-r--r--src/main-view/FieldContentCodec.h28
-rw-r--r--src/main-view/FieldContentPainter.h16
-rw-r--r--src/main-view/FindPanel.cpp258
-rw-r--r--src/main-view/FindPanel.h66
-rw-r--r--src/main-view/LanguageMenu.cpp64
-rw-r--r--src/main-view/LanguageMenu.h28
-rw-r--r--src/main-view/MainWindow.cpp1313
-rw-r--r--src/main-view/MainWindow.h218
-rw-r--r--src/main-view/PacksTreeModel.cpp93
-rw-r--r--src/main-view/PacksTreeModel.h32
-rw-r--r--src/main-view/RecentFilesManager.cpp99
-rw-r--r--src/main-view/RecentFilesManager.h43
-rw-r--r--src/main-view/RecordEditor.cpp122
-rw-r--r--src/main-view/RecordEditor.h46
-rw-r--r--src/main-view/UndoCommands.cpp322
-rw-r--r--src/main-view/UndoCommands.h109
-rw-r--r--src/main-view/WelcomeScreen.cpp51
-rw-r--r--src/main-view/WelcomeScreen.h25
37 files changed, 4156 insertions, 0 deletions
diff --git a/src/main-view/AboutDialog.cpp b/src/main-view/AboutDialog.cpp
new file mode 100644
index 0000000..f1a7839
--- /dev/null
+++ b/src/main-view/AboutDialog.cpp
@@ -0,0 +1,27 @@
+#include "AboutDialog.h"
+#include "../strings.h"
+#include "../version.h"
+
+AboutDialog::AboutDialog(QWidget* parent):
+ QMessageBox(parent)
+{
+ setIconPixmap(QPixmap(":/images/freshmemory.png"));
+ setWindowTitle(tr("About %1").arg( Strings::tr(Strings::s_appTitle)));
+ setText(createAboutText());
+ setEscapeButton(addButton(QMessageBox::Ok));
+}
+
+QString AboutDialog::createAboutText()
+{
+ QString formattedBuildStr;
+ if( !BuildStr.isEmpty() )
+ formattedBuildStr = "<p style=\"font-size:10pt\">" + BuildStr + "</p>";
+ return QString("<p><h2>") + Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "</h2></p>" +
+ formattedBuildStr +
+ "<p>" + tr("Learn new things quickly and keep your memory fresh with time spaced repetition.") + "</p>" +
+ "<p>" + Strings::tr(Strings::s_author) + "</p>" +
+ "<p><a href=\"http://fresh-memory.com\"> fresh-memory.com </a></p>" +
+ "</a></p>" +
+ "<p>" + tr("License:") + " <a href=\"http://www.gnu.org/copyleft/gpl.html\"> GPL 3" +
+ "</a></p>" + "<p><img src=\":/images/gplv3-88x31.png\"></p>";
+ }
diff --git a/src/main-view/AboutDialog.h b/src/main-view/AboutDialog.h
new file mode 100644
index 0000000..d05c11c
--- /dev/null
+++ b/src/main-view/AboutDialog.h
@@ -0,0 +1,17 @@
+#ifndef ABOUT_DIALOG_H
+#define ABOUT_DIALOG_H
+
+#include <QtWidgets>
+
+class AboutDialog: public QMessageBox
+{
+ Q_OBJECT
+
+public:
+ AboutDialog(QWidget* parent);
+
+private:
+ QString createAboutText();
+};
+
+#endif
diff --git a/src/main-view/AppModel.cpp b/src/main-view/AppModel.cpp
new file mode 100644
index 0000000..45e1956
--- /dev/null
+++ b/src/main-view/AppModel.cpp
@@ -0,0 +1,196 @@
+#include "AppModel.h"
+
+#include <stdlib.h>
+
+#include "../utils/RandomGenerator.h"
+#include "../dictionary/Dictionary.h"
+#include "../main-view/DictTableModel.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../dictionary/CardPack.h"
+
+AppModel::AppModel():
+ curDictIndex(-1), curCardPackIndex(0)
+{
+}
+
+AppModel::~AppModel()
+{
+ while(!dictionaries.isEmpty())
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeLast();
+ delete pair.second;
+ delete pair.first;
+ }
+}
+
+bool AppModel::openDictionary(const QString& filePath)
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ bool ok = dict->load(filePath);
+ if(ok)
+ addDictionary(dict);
+ else
+ {
+ errorMessage = dict->getErrorMessage();
+ delete dict;
+ }
+ return ok;
+}
+
+void AppModel::addDictionary( Dictionary* aDict )
+{
+ DictTableModel* dictModel = new DictTableModel( aDict );
+ dictionaries << qMakePair( aDict, dictModel );
+ curDictIndex = dictionaries.size() - 1;
+}
+
+Dictionary* AppModel::newDictionary()
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ dict->setDefaultFields();
+ addDictionary( dict );
+ return dict;
+}
+
+void AppModel::fixupCurDictIx()
+{
+ if( curDictIndex < 0 || curDictIndex >= dictionaries.size() )
+ curDictIndex = dictionaries.size()-1;
+}
+
+Dictionary* AppModel::curDictionary()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].first;
+ }
+
+DictTableModel* AppModel::curDictModel()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].second;
+ }
+
+Dictionary* AppModel::dictionary(int aIndex) const
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return NULL;
+ return dictionaries[aIndex].first;
+ }
+
+int AppModel::indexOfDictionary( Dictionary* aDic ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.first == aDic )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+int AppModel::indexOfDictionary( const QString& aFilePath ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ Q_ASSERT( pair.first );
+ if( pair.first->getFilePath() == aFilePath )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+CardPack* AppModel::curCardPack()
+ {
+ Dictionary* curDic = curDictionary();
+ if( curDic )
+ return curDic->cardPack( curCardPackIndex );
+ else
+ return NULL;
+ }
+
+bool AppModel::setCurDictionary(int index)
+ {
+ if( dictionaries.isEmpty() )
+ {
+ curDictIndex = -1;
+ return false;
+ }
+ if( index < 0 || index >= dictionaries.size())
+ {
+ curDictIndex = dictionaries.size()-1;
+ return false;
+ }
+ curDictIndex = index;
+ return true;
+ }
+
+bool AppModel::removeDictionary( int aIndex )
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return false;
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeAt( aIndex );
+ delete pair.second;
+ delete pair.first;
+ return true;
+ }
+
+/** Destroys dic model and the dictionary itself.
+ */
+void AppModel::removeDictModel( QAbstractItemModel* aDictModel )
+ {
+ int i = 0;
+ for( ;i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.second == aDictModel )
+ {
+ dictionaries.removeAt( i ); // Update the size of m_dictionaries before destroying
+ delete pair.first;
+ delete pair.second;
+ break;
+ }
+ }
+ }
+
+IStudyModel* AppModel::createStudyModel(int studyType, int cardPackIndex)
+ {
+ Dictionary* curDic = curDictionary();
+ if(!curDic)
+ {
+ errorMessage = tr("No dictionary opened.");
+ return NULL;
+ }
+ if(curDic->entriesNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ curCardPackIndex = cardPackIndex;
+ CardPack* cardPack = curDic->cardPack(curCardPackIndex);
+ if(!cardPack || cardPack->cardsNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ IStudyModel* studyModel = NULL;
+ switch(studyType)
+ {
+ case WordDrill:
+ studyModel = new WordDrillModel(cardPack);
+ break;
+ case SpacedRepetition:
+ studyModel = new SpacedRepetitionModel(cardPack, new RandomGenerator);
+ break;
+ }
+ if(studyModel)
+ studyModel->setDictModel( curDictModel() );
+ return studyModel;
+ }
diff --git a/src/main-view/AppModel.h b/src/main-view/AppModel.h
new file mode 100644
index 0000000..bf1ddd2
--- /dev/null
+++ b/src/main-view/AppModel.h
@@ -0,0 +1,64 @@
+#ifndef APPMODEL_H
+#define APPMODEL_H
+
+#include <QList>
+#include <QPair>
+#include <QFile>
+#include <QAbstractItemModel>
+
+class Dictionary;
+class DictTableModel;
+class CardPack;
+class IStudyModel;
+
+class AppModel: public QObject
+{
+Q_OBJECT
+
+public:
+ enum StudyType
+ {
+ WordDrill,
+ SpacedRepetition,
+ StudyTypesNum
+ };
+
+public:
+ AppModel();
+ ~AppModel();
+
+ bool openDictionary( const QString& filePath );
+ void addDictionary( Dictionary* aDict );
+ Dictionary* newDictionary();
+ Dictionary* curDictionary();
+ int getCurDictIndex() const { return curDictIndex; }
+ DictTableModel* curDictModel();
+ int dictionariesNum() const { return dictionaries.size(); }
+ Dictionary* dictionary(int aIndex) const;
+ int indexOfDictionary( Dictionary* aDic ) const;
+ int indexOfDictionary( const QString& aFilePath ) const;
+ // TODO: Synchronize this with the pack tree view selection
+ int curCardPackIx() const { return curCardPackIndex; }
+ CardPack* curCardPack();
+ QString getErrorMessage() const { return errorMessage; }
+
+public slots:
+ /** @return true, if the dictionary was successfully set. false, if there are no dictionaries, or the index is out of range.
+ * If the index is out of range, the last dictionary from the list is set.
+ */
+ bool setCurDictionary(int index);
+ bool removeDictionary(int aIndex);
+ void removeDictModel(QAbstractItemModel* aDictModel);
+ IStudyModel* createStudyModel(int studyType, int cardPackIndex);
+
+private:
+ void fixupCurDictIx();
+
+private:
+ QList< QPair<Dictionary*, DictTableModel*> > dictionaries;
+ QString errorMessage;
+ int curDictIndex;
+ int curCardPackIndex;
+};
+
+#endif
diff --git a/src/main-view/CardFilterModel.cpp b/src/main-view/CardFilterModel.cpp
new file mode 100644
index 0000000..50ac75d
--- /dev/null
+++ b/src/main-view/CardFilterModel.cpp
@@ -0,0 +1,24 @@
+#include "CardFilterModel.h"
+
+CardFilterModel::CardFilterModel( QObject* parent ):
+ QSortFilterProxyModel( parent )
+ {
+ }
+
+void CardFilterModel::addFilterRow( int aRow )
+ {
+ if( m_filterRows.contains( aRow ) )
+ return;
+ m_filterRows << aRow;
+ }
+
+void CardFilterModel::removeFilterRow( int aRow )
+ {
+ m_filterRows.removeOne( aRow );
+ }
+
+bool CardFilterModel::filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const
+ {
+ Q_UNUSED( source_parent );
+ return m_filterRows.contains( source_row );
+ }
diff --git a/src/main-view/CardFilterModel.h b/src/main-view/CardFilterModel.h
new file mode 100644
index 0000000..7c6e9f2
--- /dev/null
+++ b/src/main-view/CardFilterModel.h
@@ -0,0 +1,21 @@
+#ifndef CARDFILTERMODEL_H
+#define CARDFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+class CardFilterModel : public QSortFilterProxyModel
+ {
+ Q_OBJECT
+public:
+ CardFilterModel( QObject* parent );
+ void addFilterRow( int aRow );
+ void removeFilterRow( int aRow );
+
+protected:
+ bool filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const;
+
+private:
+ QList<int> m_filterRows;
+ };
+
+#endif // CARDFILTERMODEL_H
diff --git a/src/main-view/CardPreview.cpp b/src/main-view/CardPreview.cpp
new file mode 100644
index 0000000..aa59031
--- /dev/null
+++ b/src/main-view/CardPreview.cpp
@@ -0,0 +1,55 @@
+#include "CardPreview.h"
+#include "../study/CardSideView.h"
+#include "../dictionary/Card.h"
+
+CardPreview::CardPreview(QWidget* parent):
+ QDockWidget(parent)
+{
+ setDockProperties();
+ createCardSides();
+ createInternalWidget();
+}
+
+void CardPreview::setDockProperties()
+{
+ setWindowTitle(tr("Card preview"));
+ setObjectName("Card-preview");
+ setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+}
+
+void CardPreview::createCardSides()
+{
+ questionSide = new CardSideView(CardSideView::QstMode);
+ answerSide = new CardSideView(CardSideView::AnsMode);
+}
+
+void CardPreview::createInternalWidget()
+{
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(questionSide, 1);
+ mainLt->addWidget(answerSide, 1);
+
+ QWidget* internalWidget = new QWidget;
+ internalWidget->setLayout(mainLt);
+ setWidget(internalWidget);
+}
+
+void CardPreview::setPack(const CardPack* pack)
+{
+ questionSide->setPack(pack);
+ answerSide->setPack(pack);
+}
+
+void CardPreview::setContent(const CardPack* pack, Card* card)
+{
+ setPack(pack);
+ QString question;
+ QStringList answers;
+ if(card)
+ {
+ question = card->getQuestion();
+ answers = card->getAnswers();
+ }
+ questionSide->setQuestion(question);
+ answerSide->setQstAnsr(question, answers);
+}
diff --git a/src/main-view/CardPreview.h b/src/main-view/CardPreview.h
new file mode 100644
index 0000000..6217c96
--- /dev/null
+++ b/src/main-view/CardPreview.h
@@ -0,0 +1,30 @@
+#ifndef CARD_PREVIEW_H
+#define CARD_PREVIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class CardSideView;
+class Card;
+class CardPack;
+
+class CardPreview: public QDockWidget
+{
+ Q_OBJECT
+
+public:
+ CardPreview(QWidget* parent);
+ void setContent(const CardPack* pack, Card* card);
+
+private:
+ void createCardSides();
+ void createInternalWidget();
+ void setDockProperties();
+ void setPack(const CardPack* pack);
+
+private:
+ CardSideView* questionSide;
+ CardSideView* answerSide;
+};
+
+#endif
diff --git a/src/main-view/DictTableDelegate.cpp b/src/main-view/DictTableDelegate.cpp
new file mode 100644
index 0000000..1e1d609
--- /dev/null
+++ b/src/main-view/DictTableDelegate.cpp
@@ -0,0 +1,128 @@
+#include "DictTableDelegate.h"
+#include <QtDebug>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "DictTableDelegatePainter.h"
+#include "FieldContentCodec.h"
+#include "../dictionary/Dictionary.h"
+
+QWidget* DictTableDelegate::createEditor( QWidget* parent,
+ const QStyleOptionViewItem& option, const QModelIndex& /*index*/ ) const
+{
+ editor = new RecordEditor(parent, option.rect);
+ connect( editor, SIGNAL(destroyed()), SIGNAL(editorDestroyed()) );
+ emit editorCreated();
+ return editor;
+}
+
+void DictTableDelegate::updateEditorGeometry(QWidget* editor,
+ const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const
+{
+ setEditorData(editor, index);
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ recordEditor->updateEditor();
+}
+
+bool DictTableDelegate::eventFilter(QObject *object, QEvent *event)
+{
+ QWidget* editor = qobject_cast<QWidget*>(object);
+ if (!editor)
+ return false;
+ if(event->type() == QEvent::KeyPress)
+ switch( static_cast<QKeyEvent*>(event)->key() )
+ {
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ case Qt::Key_Tab:
+ emit commitData(editor);
+ emit closeEditor(editor, QAbstractItemDelegate::EditNextItem);
+ return true;
+ default:
+ break;
+ }
+ return QStyledItemDelegate::eventFilter(object, event);
+}
+
+void DictTableDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ FieldContentCodec codec(recordEditor);
+ codec.parse(getDisplayText(index));
+}
+
+QString DictTableDelegate::getDisplayText(const QModelIndex& index) const
+{
+ return index.data(Qt::EditRole).toString();
+}
+
+void DictTableDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const
+ {
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model );
+ QString editorText = recordEditor->getText();
+ QModelIndex origIndex = index;
+ if( !tableModel )
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model );
+ if( !proxyModel )
+ return;
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( !tableModel )
+ return;
+ origIndex = proxyModel->mapToSource( index );
+ }
+ if(editorText == getDisplayText(index) && !indexIsLastCell(origIndex, model))
+ return;
+ QString newText = tableModel->dictionary()->shortenImagePaths(editorText);
+ QUndoCommand* command = new EditRecordCmd( tableModel, origIndex, newText );
+ tableModel->undoStack()->push( command );
+}
+
+bool DictTableDelegate::indexIsLastCell(const QModelIndex& index,
+ QAbstractItemModel* model) const
+{
+ return index.row() == model->rowCount() - 1 &&
+ index.column() == model->columnCount() - 1;
+}
+
+void DictTableDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+{
+ QStyledItemDelegate::paint(painter, option, index);
+ DictTableDelegatePainter dPainter(painter, getMarginRect(option),
+ option.fontMetrics);
+ FieldContentCodec codec(&dPainter);
+ codec.parse(getDisplayText(index));
+}
+
+QRect DictTableDelegate::getMarginRect(const QStyleOptionViewItem& option) const
+{
+ const int margin = 2;
+ return option.rect.adjusted(margin, 0, -margin, 0);
+}
+
+int DictTableDelegate::getCursorPos() const
+{
+ if(editor)
+ return editor->textCursor().position();
+ else
+ return -1;
+}
+
+void DictTableDelegate::setCursorPos(int pos)
+{
+ if(!editor)
+ return;
+ QTextCursor cursor = editor->textCursor();
+ cursor.setPosition(pos);
+ editor->setTextCursor(cursor);
+}
+
+void DictTableDelegate::insertImageIntoEditor(int cursorPos, const QString& filePath)
+{
+ if(!editor)
+ return;
+ editor->insertImage(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableDelegate.h b/src/main-view/DictTableDelegate.h
new file mode 100644
index 0000000..cf43fc5
--- /dev/null
+++ b/src/main-view/DictTableDelegate.h
@@ -0,0 +1,44 @@
+#ifndef DICT_TABLE_DELEGATE_H
+#define DICT_TABLE_DELEGATE_H
+
+#include <QtWidgets>
+
+#include "RecordEditor.h"
+
+class DictTableDelegate: public QStyledItemDelegate
+{
+Q_OBJECT
+
+public:
+ DictTableDelegate(QObject* parent):
+ QStyledItemDelegate(parent), editor(NULL) {}
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ bool eventFilter(QObject *object, QEvent *event);
+ void setEditorData(QWidget* editor, const QModelIndex& index) const;
+ void setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const;
+ void paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ int getCursorPos() const;
+ void setCursorPos(int pos);
+ void insertImageIntoEditor(int cursorPos, const QString& filePath);
+ QWidget* getEditor() const { return editor; }
+
+private:
+ QString getDisplayText(const QModelIndex& index) const;
+ QRect getMarginRect(const QStyleOptionViewItem& option) const;
+ bool indexIsLastCell(const QModelIndex& index, QAbstractItemModel* model) const;
+
+signals:
+ void editorCreated() const;
+ void editorDestroyed() const;
+
+private:
+ mutable RecordEditor* editor;
+};
+
+#endif
+
diff --git a/src/main-view/DictTableDelegatePainter.cpp b/src/main-view/DictTableDelegatePainter.cpp
new file mode 100644
index 0000000..2714bdc
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.cpp
@@ -0,0 +1,61 @@
+#include "DictTableDelegatePainter.h"
+
+DictTableDelegatePainter::DictTableDelegatePainter(QPainter* painter,
+ const QRect& contentRect, const QFontMetrics& fontMetrics):
+ painter(painter), contentRect(contentRect), fontMetrics(fontMetrics)
+{
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::initLoopParams()
+{
+ offset = 0;
+ textFitstoRect = true;
+}
+
+void DictTableDelegatePainter::startDrawing()
+{
+ painter->save();
+ painter->setClipRect(contentRect);
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::endDrawing()
+{
+ painter->restore();
+}
+
+void DictTableDelegatePainter::drawText(const QString& text)
+{
+ if(!textFitstoRect)
+ return;
+ QString elidedText = getElidedText(text);
+ textFitstoRect = elidedText == text;
+ painter->drawText(contentRect.translated(offset, 0),
+ elidedText, getTextOption());
+ offset += fontMetrics.width(elidedText);
+}
+
+void DictTableDelegatePainter::drawImage(const QString& filePath)
+{
+ if(!textFitstoRect)
+ return;
+ QPixmap image(filePath);
+ if(image.isNull())
+ image = QPixmap(":/images/broken-image.png");
+ image = image.scaledToHeight(ThumbnailSize, Qt::SmoothTransformation);
+ painter->drawPixmap(contentRect.topLeft() + QPoint(offset, 0), image);
+ offset += image.width();
+}
+
+QString DictTableDelegatePainter::getElidedText(const QString& text)
+{
+ return fontMetrics.elidedText(text, Qt::ElideRight, contentRect.width() - offset);
+}
+
+QTextOption DictTableDelegatePainter::getTextOption()
+{
+ QTextOption textOption(Qt::AlignVCenter);
+ textOption.setWrapMode(QTextOption::NoWrap);
+ return textOption;
+}
diff --git a/src/main-view/DictTableDelegatePainter.h b/src/main-view/DictTableDelegatePainter.h
new file mode 100644
index 0000000..985ecac
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.h
@@ -0,0 +1,36 @@
+#ifndef DICT_TABLE_DELEGATE_PAINTER_H
+#define DICT_TABLE_DELEGATE_PAINTER_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class DictTableDelegatePainter: public FieldContentPainter
+{
+private:
+ static QTextOption getTextOption();
+
+private:
+ static const int ThumbnailSize = 25;
+
+public:
+ DictTableDelegatePainter(QPainter* painter, const QRect& contentRect,
+ const QFontMetrics& fontMetrics);
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+
+private:
+ void initLoopParams();
+ QString getElidedText(const QString& text);
+
+private:
+ QPainter* painter;
+ QRect contentRect;
+ QFontMetrics fontMetrics;
+ int offset;
+ bool textFitstoRect;
+};
+#endif
+
diff --git a/src/main-view/DictTableModel.cpp b/src/main-view/DictTableModel.cpp
new file mode 100644
index 0000000..b7b7eed
--- /dev/null
+++ b/src/main-view/DictTableModel.cpp
@@ -0,0 +1,142 @@
+#include <QtGui>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/Field.h"
+
+DictTableModel::DictTableModel( Dictionary* aDict, QObject *parent ):
+ QAbstractTableModel( parent ), m_dictionary( aDict )
+ {
+ m_undoStack = new QUndoStack( this );
+ connect( m_dictionary, SIGNAL(destroyed(QObject*)), SLOT(discardCurDictionary()) );
+ }
+
+int DictTableModel::rowCount( const QModelIndex &/*parent*/ ) const
+ {
+ if( m_dictionary )
+ return m_dictionary->entriesNum();
+ else
+ return 0;
+ }
+
+int DictTableModel::columnCount(const QModelIndex &/*parent*/) const
+ {
+ if( m_dictionary )
+ return m_dictionary->fieldsNum();
+ else
+ return 0;
+ }
+
+QVariant DictTableModel::data( const QModelIndex &index, int role ) const
+ {
+ if( !m_dictionary || !index.isValid() || index.row() >= rowCount() ||
+ index.column() >= columnCount() )
+ return QVariant();
+ QString text = m_dictionary->getFieldValue(index.row(), index.column());
+ switch(role)
+ {
+ case Qt::DisplayRole:
+ return QVariant();
+ case Qt::EditRole:
+ return m_dictionary->extendImagePaths(text);
+ case DicRecordRole:
+ {
+ const DicRecord* record = m_dictionary->getRecord(index.row());
+ return QVariant::fromValue(*record);
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+QVariant DictTableModel::headerData( int section, Qt::Orientation orientation, int role ) const
+ {
+ if( !m_dictionary )
+ return QVariant();
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ if(orientation == Qt::Vertical)
+ return QString::number( section + 1 );
+ else
+ {
+ if( section < columnCount() )
+ return m_dictionary->field( section )->name();
+ else
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+bool DictTableModel::setData( const QModelIndex &index, const QVariant &value, int role )
+ {
+ if( !index.isValid() || !m_dictionary )
+ return false;
+ if( value == data( index, role ) )
+ return false;
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ m_dictionary->setFieldValue(index.row(), index.column(), value.toString());
+ break;
+ case DicRecordRole:
+ {
+ DicRecord record = value.value<DicRecord>();
+ m_dictionary->setRecord( index.row(), record );
+ break;
+ }
+ default:
+ return false;
+ }
+ emit dataChanged( index, index );
+ return true;
+ }
+
+bool DictTableModel::insertRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->insertEntries( position, rows );
+ endInsertRows();
+ return true;
+ }
+
+bool DictTableModel::addFields( QStringList aFields )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertColumns( QModelIndex(), columnCount(), columnCount() + aFields.size() - 1 );
+ m_dictionary->addFields( aFields );
+ endInsertColumns();
+ return true;
+ }
+
+bool DictTableModel::removeRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginRemoveRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->removeRecords( position, rows );
+ endRemoveRows();
+ return true;
+ }
+
+void DictTableModel::resetData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+void DictTableModel::discardCurDictionary()
+ {
+ beginResetModel();
+ m_dictionary = NULL;
+ endResetModel();
+ }
diff --git a/src/main-view/DictTableModel.h b/src/main-view/DictTableModel.h
new file mode 100644
index 0000000..0293ace
--- /dev/null
+++ b/src/main-view/DictTableModel.h
@@ -0,0 +1,52 @@
+#ifndef DICTTABLEMODEL_H
+#define DICTTABLEMODEL_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class Dictionary;
+class DicRecord;
+
+class DictTableModel: public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum
+ {
+ DicRecordRole = Qt::UserRole
+ };
+
+ DictTableModel( Dictionary* aDict, QObject *parent = 0 );
+
+ // Getters
+
+ const Dictionary* dictionary() const { return m_dictionary; }
+ QUndoStack* undoStack() const { return m_undoStack; }
+
+ Qt::ItemFlags flags( const QModelIndex& index ) const { return QAbstractItemModel::flags( index ) | Qt::ItemIsEditable; }
+ int rowCount( const QModelIndex& parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex& parent = QModelIndex() ) const;
+ QVariant data(const QModelIndex& index, int role) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+
+ // Setters
+
+ bool setData( const QModelIndex &index, const QVariant& value, int role = Qt::EditRole );
+ bool insertRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+ bool addFields( QStringList aFields );
+ bool removeRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+
+public slots:
+ void resetData();
+
+private slots:
+ void discardCurDictionary();
+
+private:
+ static const int ThumbnailSize = 25; ///< Size of image thumbnails
+
+ Dictionary* m_dictionary; // not own
+ QUndoStack* m_undoStack;
+};
+#endif
diff --git a/src/main-view/DictTableView.cpp b/src/main-view/DictTableView.cpp
new file mode 100644
index 0000000..9d61fad
--- /dev/null
+++ b/src/main-view/DictTableView.cpp
@@ -0,0 +1,84 @@
+#include "DictTableView.h"
+#include "DictTableDelegate.h"
+
+#include <QtAlgorithms>
+#include <QHeaderView>
+#include <QAbstractProxyModel>
+
+#include "DictTableModel.h"
+
+DictTableView::DictTableView( QAbstractItemModel* aModel, QWidget* aParent ):
+ QTableView( aParent )
+ {
+ setModel( aModel );
+ connect( aModel, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)),
+ SLOT(disableUpdates()) );
+ connect( aModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(enableUpdates()) );
+ setItemDelegate( new DictTableDelegate(this) );
+ setAlternatingRowColors(true);
+ setShowGrid(true);
+ verticalHeader()->setDefaultSectionSize( RowHeight );
+ setSelectionBehavior( QAbstractItemView::SelectRows );
+ resizeColumnsToContents();
+ horizontalHeader()->stretchLastSection();
+ }
+
+DictTableView::~DictTableView()
+ {
+ emit destroyed( model() );
+ }
+
+DictTableModel* DictTableView::dicTableModel() const
+ {
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model() );
+ if( tableModel )
+ return tableModel;
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model() );
+ if( proxyModel )
+ {
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( tableModel )
+ return tableModel;
+ }
+ return NULL;
+ }
+
+void DictTableView::resizeColumnsToContents()
+ {
+ QTableView::resizeColumnsToContents();
+ for( int i=0; i<model()->columnCount(); i++ )
+ if( columnWidth( i ) < KMinColWidth )
+ setColumnWidth( i, KMinColWidth );
+ else if ( columnWidth( i ) > KMaxColWidth )
+ setColumnWidth( i, KMaxColWidth );
+}
+
+void DictTableView::startEditing(int row, int col)
+{
+ edit(model()->index(row, col));
+}
+
+void DictTableView::commitEditing()
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ commitData(delegate->getEditor());
+}
+
+int DictTableView::getEditorCursorPos() const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return -1;
+ return delegate->getCursorPos();
+}
+
+void DictTableView::insertImageIntoEditor(int cursorPos, const QString& filePath) const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ delegate->insertImageIntoEditor(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableView.h b/src/main-view/DictTableView.h
new file mode 100644
index 0000000..b34df65
--- /dev/null
+++ b/src/main-view/DictTableView.h
@@ -0,0 +1,36 @@
+#ifndef DICTTABLEVIEW_h
+#define DICTTABLEVIEW_h
+
+#include <QTableView>
+#include <QtDebug>
+
+class DictTableModel;
+
+class DictTableView: public QTableView
+{
+Q_OBJECT
+
+public:
+ DictTableView( QAbstractItemModel* aModel, QWidget* aParent = 0 );
+ virtual ~DictTableView();
+ void resizeColumnsToContents();
+ DictTableModel* dicTableModel() const;
+ void startEditing(int row, int col);
+ void commitEditing();
+ int getEditorCursorPos() const;
+ void insertImageIntoEditor(int cursorPos, const QString& filePath) const;
+
+private slots:
+ void enableUpdates() { setUpdatesEnabled(true); }
+ void disableUpdates() { setUpdatesEnabled(false); }
+
+signals:
+ void destroyed( QAbstractItemModel* aDictModel );
+
+private:
+ static const int KMinColWidth = 170;
+ static const int KMaxColWidth = 400;
+ static const int RowHeight = 27;
+};
+
+#endif
diff --git a/src/main-view/DictionaryTabWidget.cpp b/src/main-view/DictionaryTabWidget.cpp
new file mode 100644
index 0000000..94f090c
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.cpp
@@ -0,0 +1,108 @@
+#include "DictionaryTabWidget.h"
+#include "MainWindow.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+
+DictionaryTabWidget::DictionaryTabWidget(MainWindow* aMainWin):
+ QTabWidget( aMainWin ), m_mainWin( aMainWin ), createdEditorsNum(0)
+ {
+ setDocumentMode( true );
+ setTabsClosable( true );
+ setMovable( true );
+ connect( this, SIGNAL(tabCloseRequested(int)), SLOT(closeTab(int)) );
+ m_undoGroup = new QUndoGroup( this );
+
+ m_continueLbl = new QLabel( this );
+ m_continueLbl->hide();
+ m_continueLbl->setPixmap(QPixmap(":/images/continue-search.png"));
+ }
+
+
+/** The tab is removed automatically when the widget is destroyed.
+ The undo stack is removed from group automatically, when the stack is destroyed.
+ */
+void DictionaryTabWidget::closeTab( int aIndex )
+ {
+ if( aIndex == -1 )
+ aIndex = currentIndex();
+ bool canRemove = m_mainWin->proposeToSave( aIndex );
+ if( canRemove )
+ delete widget( aIndex );
+ }
+
+int DictionaryTabWidget::addDictTab( DictTableModel* aDictModel )
+ {
+ DictTableView* dictView = new DictTableView( aDictModel );
+ int tabIx = addTab( dictView, "" );
+ setCurrentIndex( tabIx );
+ QUndoStack* undoStack = aDictModel->undoStack();
+ m_undoGroup->addStack( undoStack );
+ m_undoGroup->setActiveStack( undoStack );
+ connect( undoStack, SIGNAL(cleanChanged(bool)), aDictModel->dictionary(), SLOT(setContentClean(bool)) );
+ connect( dictView->itemDelegate(), SIGNAL(editorCreated()), SLOT(createEditor()) );
+ connect( dictView->itemDelegate(), SIGNAL(editorDestroyed()), SLOT(destroyEditor()) );
+ return tabIx;
+ }
+
+const DictTableView* DictionaryTabWidget::curDictView() const
+ {
+ QWidget* curWidget = currentWidget();
+ if( !curWidget )
+ return NULL;
+ DictTableView* curView = static_cast<DictTableView*>( curWidget );
+ return curView;
+ }
+
+void DictionaryTabWidget::setCurrentIndex( int aTabIx )
+ {
+ QTabWidget::setCurrentIndex( aTabIx );
+ const DictTableView* dictView = curDictView();
+ if( dictView )
+ {
+ const DictTableModel* dictModel = dictView->dicTableModel();
+ if( dictModel )
+ m_undoGroup->setActiveStack( dictModel->undoStack() );
+ }
+ }
+
+void DictionaryTabWidget::goToDictionaryRecord( int aDictIx, int aRecordRow )
+ {
+ setCurrentIndex( aDictIx );
+ QWidget* curWidget = currentWidget();
+ Q_ASSERT( curWidget );
+ QAbstractItemView* curView = static_cast<QAbstractItemView*>( curWidget );
+ Q_ASSERT( curView );
+ curView->setFocus();
+ QModelIndex index = curView->model()->index( aRecordRow, 0 );
+ curView->setCurrentIndex( index );
+ }
+
+void DictionaryTabWidget::cleanUndoStack()
+{
+ m_undoGroup->activeStack()->setClean();
+}
+
+bool DictionaryTabWidget::undoStackIsClean() const
+{
+ return m_undoGroup->isClean();
+}
+
+void DictionaryTabWidget::showContinueSearch()
+{
+ m_continueLbl->move( rect().center() - m_continueLbl->rect().center() );
+ m_continueLbl->show();
+ QTimer::singleShot( 500, m_continueLbl, SLOT(hide()) );
+}
+
+void DictionaryTabWidget::createEditor()
+{
+ createdEditorsNum++;
+ emit editingStateChanged();
+}
+
+void DictionaryTabWidget::destroyEditor()
+{
+ createdEditorsNum--;
+ emit editingStateChanged();
+}
diff --git a/src/main-view/DictionaryTabWidget.h b/src/main-view/DictionaryTabWidget.h
new file mode 100644
index 0000000..b5bd3c7
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.h
@@ -0,0 +1,46 @@
+#ifndef DICTIONARYTABWIDGET_H
+#define DICTIONARYTABWIDGET_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class MainWindow;
+class FilterBar;
+class DictTableModel;
+class DictTableView;
+class Dictionary;
+
+class DictionaryTabWidget : public QTabWidget
+{
+ Q_OBJECT
+public:
+ DictionaryTabWidget(MainWindow* aMainWin);
+
+ int addDictTab( DictTableModel* aDictModel );
+ void setCurrentIndex( int aTabIx );
+ void goToDictionaryRecord( int aDictIx, int aRecordRow );
+ void cleanUndoStack();
+ void showContinueSearch();
+
+ const DictTableView* curDictView() const;
+ QUndoGroup* undoGroup() const { return m_undoGroup; }
+ bool undoStackIsClean() const;
+ bool isInEditingState() const { return createdEditorsNum > 0; }
+
+public slots:
+ void closeTab( int aIndex = -1 );
+
+private slots:
+ void createEditor();
+ void destroyEditor();
+
+signals:
+ void editingStateChanged();
+
+private:
+ MainWindow* m_mainWin; // parent, now own
+ QUndoGroup* m_undoGroup;
+ QLabel* m_continueLbl; // Continue search icon
+ int createdEditorsNum;
+};
+#endif // DICTIONARYTABWIDGET_H
diff --git a/src/main-view/FieldContentCodec.cpp b/src/main-view/FieldContentCodec.cpp
new file mode 100644
index 0000000..ac9e6c5
--- /dev/null
+++ b/src/main-view/FieldContentCodec.cpp
@@ -0,0 +1,52 @@
+#include "FieldContentCodec.h"
+
+#include "FieldContentPainter.h"
+
+FieldContentCodec::FieldContentCodec(FieldContentPainter* painter):
+ painter(painter),
+ imageRx("<img\\s+src=\"([^\"]+)\"\\s*/?>")
+{
+ initLoopParams();
+}
+
+void FieldContentCodec::initLoopParams()
+{
+ textPos = 0;
+ prevTextPos = 0;
+}
+
+void FieldContentCodec::parse(const QString& text)
+{
+ painter->startDrawing();
+ initLoopParams();
+ this->text = text;
+
+ while(findNextImage() >= 0)
+ {
+ drawTextChunk(textPos - prevTextPos);
+ drawImage();
+ textPos += imageRx.matchedLength();
+ prevTextPos = textPos;
+ }
+ drawTextChunk(-1);
+ painter->endDrawing();
+}
+
+int FieldContentCodec::findNextImage()
+{
+ textPos = imageRx.indexIn(text, textPos);
+ return textPos;
+}
+
+void FieldContentCodec::drawTextChunk(int len)
+{
+ painter->drawText(text.mid(prevTextPos, len));
+}
+
+void FieldContentCodec::drawImage()
+{
+ QString imagePath = imageRx.cap(1);
+ if(!QFileInfo(imagePath).exists())
+ imagePath = ":/images/broken-image.png";
+ painter->drawImage(imagePath);
+}
diff --git a/src/main-view/FieldContentCodec.h b/src/main-view/FieldContentCodec.h
new file mode 100644
index 0000000..8a13dba
--- /dev/null
+++ b/src/main-view/FieldContentCodec.h
@@ -0,0 +1,28 @@
+#ifndef FIELD_CONTENT_CODEC_H
+#define FIELD_CONTENT_CODEC_H
+
+#include <QtCore>
+
+class FieldContentPainter;
+
+class FieldContentCodec
+{
+public:
+ FieldContentCodec(FieldContentPainter* painter);
+ void parse(const QString& text);
+
+private:
+ void initLoopParams();
+ int findNextImage();
+ void drawTextChunk(int len);
+ void drawImage();
+
+private:
+ FieldContentPainter* painter;
+ QString text;
+ QRegExp imageRx;
+ int textPos;
+ int prevTextPos;
+};
+#endif
+
diff --git a/src/main-view/FieldContentPainter.h b/src/main-view/FieldContentPainter.h
new file mode 100644
index 0000000..eb0679a
--- /dev/null
+++ b/src/main-view/FieldContentPainter.h
@@ -0,0 +1,16 @@
+#ifndef FIELD_CONTENT_PAINTER_H
+#define FIELD_CONTENT_PAINTER_H
+
+#include <QtCore>
+
+class FieldContentPainter
+{
+public:
+ virtual ~FieldContentPainter() {}
+ virtual void startDrawing() = 0;
+ virtual void endDrawing() = 0;
+ virtual void drawText(const QString& text) = 0;
+ virtual void drawImage(const QString& filePath) = 0;
+};
+#endif
+
diff --git a/src/main-view/FindPanel.cpp b/src/main-view/FindPanel.cpp
new file mode 100644
index 0000000..755fa50
--- /dev/null
+++ b/src/main-view/FindPanel.cpp
@@ -0,0 +1,258 @@
+#include "FindPanel.h"
+
+#include <QHBoxLayout>
+#include <QAction>
+#include <QLineEdit>
+#include <QKeyEvent>
+
+#include "DictTableView.h"
+#include "MainWindow.h"
+
+FindPanel::FindPanel( MainWindow* aMainWindow ):
+ QWidget( aMainWindow ),
+ m_mainWindow( aMainWindow ), m_direction( 1 ), m_foundOnce(false)
+ {
+ m_closeButton = new QToolButton;
+ m_closeButton->setAutoRaise(true);
+ m_closeButton->setIcon(QIcon(":/images/gray-cross.png"));
+ m_closeButton->setToolTip(tr("Close"));
+
+ QLabel* textLabel = new QLabel(tr("Find:", "Title of the find pane"));
+
+ m_textEdit = new QComboBox;
+ textLabel->setBuddy(m_textEdit);
+ m_textEdit->setEditable(true);
+ m_textEdit->setInsertPolicy( QComboBox::NoInsert );
+ m_textEdit->setMaxCount( ComboBoxMaxItems );
+ m_textEdit->setMinimumWidth( TextEditMinWidth );
+ m_textEdit->setFocus();
+
+ m_findBackwardBtn = new QToolButton;
+ m_findBackwardBtn->resize(32, 32);
+ m_findBackwardBtn->setObjectName("backward");
+ m_findBackwardBtn->setIcon(QIcon(":/images/1leftarrow.png"));
+ m_findBackwardBtn->setToolTip(tr("Find previous"));
+ m_findBackwardBtn->setEnabled(false);
+
+ m_findForwardBtn = new QToolButton;
+ m_findForwardBtn->setObjectName("forward");
+ m_findForwardBtn->setIcon(QIcon(":/images/1rightarrow.png"));
+ m_findForwardBtn->setToolTip(tr("Find next"));
+ m_findForwardBtn->setEnabled(false);
+
+ m_caseSensitiveBtn = new QToolButton;
+ m_caseSensitiveBtn->setAutoRaise(true);
+ m_caseSensitiveBtn->setCheckable( true );
+ m_caseSensitiveBtn->setIcon(QIcon(":/images/Aa.png"));
+ m_caseSensitiveBtn->setToolTip(tr("Case sensitive"));
+
+ m_wholeWordsBtn = new QToolButton;
+ m_wholeWordsBtn->setAutoRaise(true);
+ m_wholeWordsBtn->setCheckable( true );
+ m_wholeWordsBtn->setIcon(QIcon(":/images/whole-words.png"));
+ m_wholeWordsBtn->setToolTip(tr("Whole words"));
+
+ m_regExpBtn = new QToolButton;
+ m_regExpBtn->setAutoRaise(true);
+ m_regExpBtn->setCheckable( true );
+ m_regExpBtn->setIcon(QIcon(":/images/RX.png"));
+ m_regExpBtn->setToolTip(tr("Regular expression"));
+
+ m_inSelectionBtn = new QToolButton;
+ m_inSelectionBtn->setAutoRaise(true);
+ m_inSelectionBtn->setCheckable( true );
+ m_inSelectionBtn->setIcon(QIcon(":/images/selection.png"));
+ m_inSelectionBtn->setToolTip(tr("In selection"));
+
+ QLabel* infoIconLbl = new QLabel;
+ infoIconLbl->setPixmap( QPixmap(":/images/warning.png").scaled(
+ 16, 16, Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
+ m_infoLbl = new QLabel(tr("String is not found"));
+ QHBoxLayout* infoLt = new QHBoxLayout;
+ infoLt->setContentsMargins( QMargins() );
+ infoLt->addWidget( infoIconLbl );
+ infoLt->addWidget( m_infoLbl );
+ m_infoPane = new QWidget;
+ m_infoPane->setLayout( infoLt );
+ m_infoPane->hide();
+
+ QHBoxLayout* mainLayout = new QHBoxLayout;
+ mainLayout->addWidget( m_closeButton );
+ mainLayout->addWidget( textLabel );
+ mainLayout->addWidget( m_textEdit );
+ mainLayout->addWidget( m_findBackwardBtn );
+ mainLayout->addWidget( m_findForwardBtn );
+ mainLayout->addWidget( m_caseSensitiveBtn );
+ mainLayout->addWidget( m_wholeWordsBtn );
+ mainLayout->addWidget( m_regExpBtn );
+ mainLayout->addWidget( m_inSelectionBtn );
+ mainLayout->addSpacing( 50 );
+ mainLayout->addWidget( m_infoPane );
+ mainLayout->addStretch();
+ mainLayout->setContentsMargins( QMargins() );
+ setLayout( mainLayout );
+
+ connect( m_inSelectionBtn, SIGNAL(toggled(bool)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), m_infoPane, SLOT(hide()) );
+ connect( m_findForwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_findBackwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_closeButton, SIGNAL(clicked()), this, SLOT(hide()) );
+ }
+
+void FindPanel::show()
+ {
+ m_textEdit->setFocus();
+ m_textEdit->lineEdit()->selectAll();
+ QWidget::show();
+ }
+
+void FindPanel::keyPressEvent( QKeyEvent* event )
+ {
+ if( event->key() == Qt::Key_Return )
+ find();
+ else
+ QWidget::keyPressEvent( event );
+ }
+
+/**
+ @arg aDirection 1 = forward; -1 = backward; 0 = previous direction
+ */
+void FindPanel::find()
+ {
+ // Check controls
+ QString searchText = m_textEdit->currentText();
+ if( searchText.isEmpty() )
+ return;
+
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndexList rows = tableView->selectionModel()->selectedRows();
+ if( rows.size() <= 1 && m_inSelectionBtn->isChecked() )
+ m_inSelectionBtn->setChecked( false );
+
+ // Process search parameters
+ bool inSelection = m_inSelectionBtn->isChecked();
+ bool fromCursor = true;
+ if( inSelection || (rows.size() == 1 && rows.first().row() == 0 ) )
+ fromCursor = false;
+
+ // Process direction
+ if( sender() )
+ {
+ if( sender()->objectName() == "forward" )
+ m_direction = 1;
+ else if( sender()->objectName() == "backward" )
+ m_direction = -1;
+ }
+ // For "find again" case, the direction stays the same
+
+ // Save the entered text to combobox
+ int textIndex = m_textEdit->findText( searchText );
+ if( textIndex != 0 ) // Don't re-add the same text at the first line
+ {
+ if( textIndex > -1 ) // Remove duplicates
+ m_textEdit->removeItem( textIndex );
+ m_textEdit->insertItem( 0, searchText );
+ }
+
+ // Create the search regular expression
+ QString searchRegExpStr;
+ if( !m_regExpBtn->isChecked() )
+ searchRegExpStr = QRegExp::escape( searchText );
+ else
+ searchRegExpStr = searchText;
+ if( m_wholeWordsBtn->isChecked() )
+ searchRegExpStr = "\\b" + searchText + "\\b";
+
+ QRegExp searchRegExp = QRegExp( searchRegExpStr,
+ m_caseSensitiveBtn->isChecked()? Qt::CaseSensitive : Qt::CaseInsensitive);
+
+ // Get sorted search range
+ QModelIndexList searchRange;
+ if( inSelection )
+ {
+ searchRange = tableView->selectionModel()->selectedIndexes();
+ qSort( searchRange );
+ }
+ else // all indexes
+ {
+ QAbstractItemModel* tableModel = tableView->model();
+ for(int r=0; r < tableModel->rowCount(); r++)
+ for(int c=0; c < tableModel->columnCount(); c++)
+ searchRange << tableModel->index(r, c);
+ }
+
+ // Get the starting search point (iterator)
+ QListIterator<QModelIndex> startingPoint( searchRange );
+ if( fromCursor )
+ {
+ bool ok = startingPoint.findNext( tableView->currentIndex() );
+ if( !ok )
+ startingPoint.toFront();
+ if( ok && m_direction < 0 )
+ startingPoint.previous(); // Go one item backwards
+ }
+ else
+ if( m_direction < 0 )
+ startingPoint.toBack();
+
+ // Try to find the regexp
+ bool found = findRegExp( searchRegExp, startingPoint );
+ if ( !found && fromCursor )
+ {
+ // Continue searching
+ m_mainWindow->showContinueSearch();
+ if( m_direction > 0 )
+ startingPoint.toFront();
+ else
+ startingPoint.toBack();
+ if( findRegExp( searchRegExp, startingPoint ) )
+ found = true;
+ }
+ if( !found )
+ m_infoPane->show();
+ }
+
+bool FindPanel::findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint )
+ {
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndex foundIndex;
+ while( (m_direction > 0)? aStartingPoint.hasNext() : aStartingPoint.hasPrevious() )
+ {
+ QModelIndex index = (m_direction > 0)? aStartingPoint.next() : aStartingPoint.previous();
+ QString valueStr = index.data( Qt::EditRole ).toString(); // Search in display, not edit, strings. Matters for <img>.
+ if( valueStr.contains( aSearchRegExp ) )
+ {
+ foundIndex = index;
+ break;
+ }
+ }
+ if( foundIndex.isValid() )
+ {
+ tableView->setFocus();
+ tableView->setCurrentIndex( foundIndex );
+ return true;
+ }
+ else
+ return false;
+ }
+
+void FindPanel::updateFindButtons()
+ {
+ m_findForwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() );
+ m_findBackwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() && !m_inSelectionBtn->isChecked() );
+ }
+
+bool FindPanel::canFindAgain()
+ {
+ if( !m_foundOnce )
+ return false;
+ const DictTableView* tableView = m_mainWindow->getCurDictView();
+ if( tableView && !m_textEdit->currentText().isEmpty() )
+ return true;
+ else
+ return false;
+ }
+
+
+
diff --git a/src/main-view/FindPanel.h b/src/main-view/FindPanel.h
new file mode 100644
index 0000000..553522e
--- /dev/null
+++ b/src/main-view/FindPanel.h
@@ -0,0 +1,66 @@
+#ifndef FINDPANEL_H
+#define FINDPANEL_H
+
+#include <QWidget>
+#include <QString>
+#include <QModelIndex>
+#include <QComboBox>
+#include <QPushButton>
+#include <QToolButton>
+#include <QCheckBox>
+#include <QMenu>
+#include <QLabel>
+
+class DictTableView;
+class MainWindow;
+
+/**
+ * Modern find panel for dictionary records.
+ */
+
+class FindPanel: public QWidget
+{
+ Q_OBJECT
+public:
+ FindPanel( MainWindow* aMainWindow );
+
+ void setTableView( DictTableView* aTableView );
+ bool canFindAgain();
+
+protected:
+ void keyPressEvent( QKeyEvent* event );
+
+private:
+ bool findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint );
+
+public slots:
+ void show();
+ void find();
+
+private slots:
+ void updateFindButtons();
+
+private:
+ static const int ComboBoxMaxItems = 10;
+ static const int TextEditMinWidth = 300;
+
+private:
+ // Data
+ MainWindow* m_mainWindow;
+ int m_direction; // Search direction: 1 or -1
+ bool m_foundOnce; // The search text was already found once. For "Find again".
+
+ // GUI
+ QToolButton* m_closeButton;
+ QComboBox* m_textEdit;
+ QToolButton* m_findForwardBtn;
+ QToolButton* m_findBackwardBtn;
+ QToolButton* m_caseSensitiveBtn;
+ QToolButton* m_wholeWordsBtn;
+ QToolButton* m_regExpBtn;
+ QToolButton* m_inSelectionBtn;
+ QWidget* m_infoPane;
+ QLabel* m_infoLbl;
+};
+
+#endif
diff --git a/src/main-view/LanguageMenu.cpp b/src/main-view/LanguageMenu.cpp
new file mode 100644
index 0000000..84ffd46
--- /dev/null
+++ b/src/main-view/LanguageMenu.cpp
@@ -0,0 +1,64 @@
+#include "LanguageMenu.h"
+#include "../main.h"
+#include "../strings.h"
+
+LanguageMenu::LanguageMenu(QMenu* parent):
+ QMenu(tr("&Language"), parent),
+ locale(QSettings().value("lang").toString()),
+ systemLocale(QLocale::system().name().split("_").first())
+{
+ initUi(parent);
+ initLangs();
+ createActionsGroup();
+ foreach(QString key, langs.keys())
+ createAction(key);
+}
+
+void LanguageMenu::initUi(QMenu* parent)
+{
+ parent->addMenu(this);
+ setIcon(QIcon(":/images/language.png"));
+}
+
+void LanguageMenu::initLangs()
+{
+ langs["cs"] = "Čeština (Czech)";
+ langs["fi"] = "Suomi (Finnish)";
+ langs["fr"] = "Français (French)";
+ langs["de"] = "Deutsch (German)";
+ langs["en"] = "English";
+ langs["ru"] = "Русский (Russian)";
+ langs["es"] = "Español (Spanish)";
+ langs["uk"] = "Українська (Ukrainian)";
+ langs[""] = tr("System") + ": " + langs[systemLocale];
+}
+
+void LanguageMenu::createActionsGroup()
+{
+ actionsGroup = new QActionGroup(this);
+ actionsGroup->setExclusive(true);
+}
+
+void LanguageMenu::createAction(const QString& key)
+{
+ QAction* action = addAction(langs[key]);
+ action->setCheckable(true);
+ if(key == locale)
+ action->setChecked(true);
+ action->setData(key);
+ actionsGroup->addAction(action);
+ connect(action, SIGNAL(triggered()), SLOT(saveLanguage()));
+}
+
+void LanguageMenu::saveLanguage()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ QString newLocale = action->data().toString();
+ QSettings().setValue("lang", newLocale);
+ if(newLocale.isEmpty())
+ newLocale = systemLocale;
+
+ installTranslator("freshmemory_" + newLocale, getResourcePath() + "/tr");
+ QMessageBox::information(this, Strings::tr(Strings::s_appTitle),
+ tr("The application must be restarted to use the selected language"));
+}
diff --git a/src/main-view/LanguageMenu.h b/src/main-view/LanguageMenu.h
new file mode 100644
index 0000000..d03984d
--- /dev/null
+++ b/src/main-view/LanguageMenu.h
@@ -0,0 +1,28 @@
+#ifndef LANGUAGE_MENU_H
+#define LANGUAGE_MENU_H
+
+#include <QtWidgets>
+
+class LanguageMenu: public QMenu
+{
+ Q_OBJECT
+public:
+ LanguageMenu(QMenu* parent);
+
+private:
+ void initUi(QMenu* parent);
+ void initLangs();
+ void createActionsGroup();
+ void createAction(const QString& key);
+
+private slots:
+ void saveLanguage();
+
+private:
+ QMap<QString, QString> langs;
+ QString locale;
+ QString systemLocale;
+ QActionGroup* actionsGroup;
+};
+
+#endif
diff --git a/src/main-view/MainWindow.cpp b/src/main-view/MainWindow.cpp
new file mode 100644
index 0000000..e5b285c
--- /dev/null
+++ b/src/main-view/MainWindow.cpp
@@ -0,0 +1,1313 @@
+#include "MainWindow.h"
+#include "AppModel.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "FindPanel.h"
+#include "DictTableDelegate.h"
+#include "PacksTreeModel.h"
+#include "AboutDialog.h"
+#include "RecentFilesManager.h"
+#include "LanguageMenu.h"
+#include "CardPreview.h"
+#include "WelcomeScreen.h"
+#include "../version.h"
+#include "../strings.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/DicCsvWriter.h"
+#include "../dictionary/DicCsvReader.h"
+#include "../dic-options/DictionaryOptionsDialog.h"
+#include "../export-import/CsvExportDialog.h"
+#include "../export-import/CsvImportDialog.h"
+#include "../settings/FontColorSettingsDialog.h"
+#include "../settings/StudySettingsDialog.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../study/WordDrillWindow.h"
+#include "../study/SpacedRepetitionWindow.h"
+#include "../study/CardSideView.h"
+#include "../utils/RandomGenerator.h"
+#include "../statistics/StatisticsView.h"
+
+MainWindow::MainWindow(AppModel* model):
+ workPath(""),
+ addImagePath(""),
+ model(model), welcomeScreen(NULL), studyWindow(NULL)
+{
+ init();
+ createCentralWidget();
+ createActions();
+ createMenus();
+ createToolBars();
+ createStatusBar();
+ FieldStyleFactory::inst()->load();
+ createDockWindows();
+ readSettings();
+ openSession();
+}
+
+MainWindow::~MainWindow()
+{
+ delete studyWindow;
+}
+
+void MainWindow::init()
+{
+ setWindowTitle(Strings::tr(Strings::s_appTitle));
+ setWindowIcon(QIcon(":/images/freshmemory.png"));
+}
+
+void MainWindow::activate()
+{
+ activateWindow();
+ raise();
+}
+
+void MainWindow::updateDictTab()
+{
+ int curIndex = dictTabWidget->currentIndex();
+ const Dictionary* curDict = model->curDictionary();
+ if(!curDict)
+ return;
+ if(!curDict->contentModified())
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/openbook-24.png") );
+ else
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/filesave.png") );
+ dictTabWidget->setTabText(curIndex, curDict->shortName());
+ dictTabWidget->setTabToolTip(curIndex, QDir::toNativeSeparators( curDict->getFilePath()));
+}
+
+void MainWindow::updateTotalRecordsLabel()
+{
+ const Dictionary* curDict = model->curDictionary();
+ if(curDict)
+ {
+ totalRecordsLabel->show();
+ totalRecordsLabel->setText( tr("Records: %1").arg(curDict->entriesNum()) );
+ }
+ else
+ totalRecordsLabel->hide();
+}
+
+void MainWindow::updateActions()
+{
+ const Dictionary* curDict = model->curDictionary();
+ bool isCurDictModified = curDict && curDict->contentModified();
+
+ saveAct->setEnabled( isCurDictModified );
+ saveAsAct->setEnabled( curDict );
+ saveCopyAct->setEnabled( curDict );
+ exportAct->setEnabled( curDict );
+ removeTabAct->setEnabled( curDict );
+ updatePasteAction();
+ insertRecordsAct->setEnabled( curDict );
+ findAct->setEnabled( curDict );
+ if( findPanel )
+ findAgainAct->setEnabled( findPanel->canFindAgain() );
+ wordDrillAct->setEnabled(curDict);
+ spacedRepetitionAct->setEnabled(curDict);
+ statisticsAct->setEnabled(curDict);
+ dictionaryOptionsAct->setEnabled(curDict);
+}
+
+void MainWindow::updateAddImageAction()
+{
+ addImageAct->setEnabled(dictTabWidget->isInEditingState());
+}
+
+void MainWindow::updateSelectionActions()
+ {
+ if( !model->curDictionary() )
+ return;
+ const DictTableView* tableView = getCurDictView();
+ if( !tableView )
+ return;
+ Q_ASSERT( tableView->selectionModel() );
+ bool hasSelection = tableView->selectionModel()->hasSelection();
+ foreach( QAction* action, selectionActions )
+ action->setEnabled( hasSelection );
+ }
+
+void MainWindow::updatePasteAction()
+{
+ bool isCurTabValid = dictTabWidget->currentIndex() >= 0;
+ bool hasClipboardText = !QApplication::clipboard()->text().isEmpty();
+ pasteAct->setEnabled( isCurTabValid && hasClipboardText );
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+if( proposeToSave() )
+ {
+ writeSettings();
+ event->accept();
+ qApp->quit();
+ }
+else
+ event->ignore();
+}
+
+void MainWindow::newFile()
+ {
+ addDictTab(model->newDictionary());
+ updatePacksTreeView();
+ }
+
+void MainWindow::openFileWithDialog()
+ {
+ openFileWithDialog(workPath);
+ }
+
+void MainWindow::openOnlineDictionaries()
+ {
+ QDesktopServices::openUrl(QUrl("http://fresh-memory.com/dictionaries"));
+ }
+
+void MainWindow::openFileWithDialog(const QString& dirPath)
+ {
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Open dictionary"), dirPath,
+ tr("Dictionaries", "Filter name in dialog")+ " (*.fmd)");
+ if( filePath.isEmpty() )
+ return;
+ openFile(filePath);
+ }
+
+void MainWindow::openFile(const QString& filePath)
+ {
+ QString internalFilePath = QDir::fromNativeSeparators(filePath);
+ workPath = QFileInfo(internalFilePath).path();
+ int dictIndex = model->indexOfDictionary(internalFilePath);
+ if(isDictOpened(dictIndex))
+ setCurDictionary(dictIndex);
+ else
+ if(!loadFile(internalFilePath))
+ return;
+ updateAfterOpenedDictionary(internalFilePath);
+ }
+
+bool MainWindow::isDictOpened(int index)
+{
+ return index > -1;
+}
+
+void MainWindow::setCurDictionary(int index)
+{
+ if(index == -1)
+ return;
+ model->setCurDictionary(index);
+ setCurDictTab(index);
+}
+
+bool MainWindow::loadFile(const QString& filePath)
+{
+ bool ok = model->openDictionary(filePath);
+ if(ok)
+ addDictTab(model->curDictionary());
+ else
+ showOpenFileError();
+ return ok;
+}
+
+void MainWindow::showOpenFileError()
+{
+ QMessageBox::critical(this, Strings::errorTitle(), model->getErrorMessage());
+}
+
+void MainWindow::updateAfterOpenedDictionary(const QString& filePath)
+{
+ recentFilesMan->addFile(filePath);
+ updatePacksTreeView();
+ updateCardPreview();
+}
+
+void MainWindow::addDictTab(Dictionary* dict)
+ {
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateActions()) );
+ connect( dict, SIGNAL(studyModifiedChanged(bool)), SLOT(saveStudyWithDelay(bool)) );
+ connect( dict, SIGNAL(filePathChanged()), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(entriesInserted(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(entriesRemoved(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updatePacksTreeView()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updateCardPreview()) );
+ connect( dict, SIGNAL(destroyed()), SLOT(updatePacksTreeView()) );
+
+ dictTabWidget->addDictTab(model->curDictModel());
+ DictTableView* tableView = const_cast<DictTableView*>( dictTabWidget->curDictView() );
+
+ tableView->addActions(contextMenuActions);
+ tableView->verticalHeader()->addActions(contextMenuActions);
+ tableView->setContextMenuPolicy( Qt::ActionsContextMenu );
+ tableView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu );
+
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateSelectionActions()) );
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateCardPreview()) );
+ connect( tableView, SIGNAL(destroyed(QAbstractItemModel*)), model, SLOT(removeDictModel(QAbstractItemModel*)) );
+ updateDictTab();
+ updateTotalRecordsLabel();
+ updateSelectionActions();
+ packsTreeView->reset();
+ QModelIndex firstIx = tableView->model()->index(0, 0, QModelIndex());
+ tableView->setCurrentIndex(firstIx);
+ tableView->setFocus();
+ }
+
+const DictTableView* MainWindow::getCurDictView() const
+ {
+ if(studyWindow)
+ {
+ const DictTableView* editCardView = studyWindow->cardEditView();
+ if( editCardView )
+ return editCardView;
+ }
+ return dictTabWidget->curDictView();
+ }
+
+/// Fork to SaveAs() or really save in DoSave()
+bool MainWindow::Save()
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() || dict->nameIsTemp() )
+ return SaveAs();
+else
+ return doSave( filePath );
+}
+
+bool MainWindow::SaveAs( bool aChangeFilePath )
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() )
+ filePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+filePath = QFileDialog::getSaveFileName(this, tr("Save dictionary as ..."), filePath);
+if( filePath.isEmpty() )
+ return false;
+workPath = QFileInfo( filePath ).path();
+if( doSave( filePath, aChangeFilePath ) )
+ {
+ recentFilesMan->addFile(filePath);
+ return true;
+ }
+else
+ return false;
+}
+
+void MainWindow::SaveCopy()
+{
+ SaveAs( false ); // Do not change the file name
+}
+
+void MainWindow::importFromCsv()
+{
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Import CSV file"), workPath);
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ CsvImportDialog importDialog( this, filePath, model );
+ if( importDialog.exec() )
+ {
+ model->addDictionary( importDialog.getDictionary() );
+ addDictTab(importDialog.getDictionary());
+ importDialog.getDictionary()->generateCards();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::exportToCsv()
+{
+ Dictionary* dict = model->curDictionary();
+ if( !dict )
+ return;
+ CsvExportDialog exportDialog( this, dict );
+ if( exportDialog.exec() )
+ {
+ QString dictFilePath( dict->getFilePath() );
+ if( dictFilePath.isEmpty() )
+ dictFilePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+ QString filePath = QFileDialog::getSaveFileName( this, tr("Export to CSV file"), dictFilePath + ".txt" );
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ exportDialog.SaveCSVToFile( filePath );
+ }
+}
+
+bool MainWindow::doSave( const QString& aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = model->curDictionary()->save( aFilePath, aChangeFilePath );
+ if( error != QFile::NoError )
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), (tr("Cannot save dictionary:") + "\n%1").
+ arg(QDir::toNativeSeparators(aFilePath)) );
+ return false;
+ }
+ dictTabWidget->cleanUndoStack();
+ updateDictTab();
+ updateActions();
+ return true;
+}
+
+bool MainWindow::saveStudy()
+{
+ QTime saveTime;
+ saveTime.start();
+ Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return false;
+ if(dict->saveStudy() != QFile::NoError)
+ {
+ QMessageBox::warning(this, Strings::errorTitle(),
+ (tr("Cannot save study file:") + "\n%1").
+ arg(QDir::toNativeSeparators(
+ model->curDictionary()->getStudyFilePath())));
+ return false;
+ }
+ return true;
+}
+
+void MainWindow::saveStudyWithDelay(bool studyModified)
+{
+ if(studyModified)
+ QTimer::singleShot(AutoSaveStudyInterval, this, SLOT(saveStudy()));
+}
+
+/**
+ * Checks if all files are saved. If not, proposes to the user to save them.
+ * This function is called before closing the application.
+ * @return true, if all files are now saved and the application can be closed
+ */
+bool MainWindow::proposeToSave()
+{
+for(int i=0; i<dictTabWidget->count(); i++)
+ if( !proposeToSave(i) )
+ return false;
+return true;
+}
+
+/**
+ * Checks if the specified file is modified and if it is, proposes to the user to save it.
+ * If the specified file is not modified, the function just returns true.
+ *
+ * Function silently saves the study data, if it is modified.
+ *
+ * @param aTabIndex Index of the tab containing the modified file
+ * @return true, if the file is saved and the application can be closed.
+ * false, if the user declined the proposition to save the file.
+ */
+bool MainWindow::proposeToSave(int aTabIndex)
+{
+const Dictionary* dict = model->dictionary( aTabIndex );
+
+if( dict->contentModified() )
+ {
+ dictTabWidget->setCurrentIndex( aTabIndex );
+ QMessageBox::StandardButton pressedButton;
+ pressedButton = QMessageBox::warning( this, tr("Save dictionary?"), tr("Dictionary %1 was modified.\n"
+ "Save changes?").arg( dict->shortName(false) ),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel );
+ if( pressedButton == QMessageBox::Yes )
+ {
+ bool saved = Save();
+ if( !saved )
+ return false;
+ }
+ else if( pressedButton == QMessageBox::Cancel )
+ return false;
+ }
+if(dict->studyModified())
+ saveStudy();
+return true;
+}
+
+void MainWindow::openSession()
+{
+ QSettings settings;
+ QStringList sessionFiles = settings.value("session").toStringList();
+ if(sessionFiles.isEmpty())
+ {
+ QString lastFile = recentFilesMan->getLastUsedFilePath();
+ if(lastFile.isEmpty())
+ return;
+ sessionFiles << recentFilesMan->getLastUsedFilePath();
+ }
+ foreach(QString fileName, sessionFiles)
+ openFile(fileName);
+ int curTab = settings.value("session-cur-tab").toInt();
+ setCurDictionary(curTab);
+}
+
+void MainWindow::help()
+ {
+ QString fullVersion = QString(FM_VERSION);
+ QString version = fullVersion.left(fullVersion.lastIndexOf('.'));
+ QString url = QString("http://fresh-memory.com/docs/%1/index.html").arg(version);
+ QDesktopServices::openUrl(QUrl(url));
+ }
+
+void MainWindow::about()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+ dialog.setParent(NULL);
+}
+
+void MainWindow::addImage()
+ {
+ int cursorPos = getCurEditorCursorPos();
+ if(cursorPos < 0)
+ return;
+ const_cast<DictTableView*>(getCurDictView())->commitEditing();
+ QString selectedFile = selectAddImageFile();
+ if(selectedFile.isEmpty())
+ return;
+ selectedFile = copyImageFileToImagesDir(selectedFile);
+ if(selectedFile.isEmpty())
+ return;
+ insertImageIntoCurEditor(cursorPos, selectedFile);
+ }
+
+QString MainWindow::selectAddImageFile()
+ {
+ checkAddImagePath();
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Add image"),
+ addImagePath,
+ tr("Images", "Filter name in dialog") +
+ " (*.png *.jpg *.jpeg *.gif *.svg *.xpm *.ico *.mng *.tiff);; " +
+ tr("All files") + " (*.*)");
+ if(filePath.isEmpty())
+ return QString();
+ addImagePath = QFileInfo(filePath).path();
+ return filePath;
+ }
+
+void MainWindow::checkAddImagePath()
+{
+ if(!addImagePath.isEmpty())
+ return;
+ if(!getCurDict())
+ return;
+ QString dicFilePath = getCurDict()->getFilePath();
+ addImagePath = QFileInfo(dicFilePath).path();
+}
+
+QString MainWindow::copyImageFileToImagesDir(const QString& filePath)
+{
+ QString dicImagesDir = getCurDict()->getImagesPath();
+ QString imageDir = QFileInfo(filePath).path();
+ if(imageDir == dicImagesDir)
+ return filePath;
+ QString newFilePath = createNewImageFilePath(dicImagesDir, filePath);
+ if(!QFileInfo(dicImagesDir).exists())
+ QDir(dicImagesDir).mkpath(".");
+ if(!QFile::copy(filePath, newFilePath))
+ return QString();
+ return newFilePath;
+}
+
+QString MainWindow::createNewImageFilePath(const QString& dicImagesDir,
+ const QString& filePath)
+{
+ int num = 0;
+ QString newFilePath;
+ do
+ {
+ newFilePath = createImagesDirFilePath(dicImagesDir, filePath, num);
+ num++;
+ }
+ while(QFileInfo(newFilePath).exists());
+ return newFilePath;
+}
+
+QString MainWindow::createImagesDirFilePath(const QString& dicImagesDir,
+ const QString& filePath, int suffixNum)
+{
+ QString imageFileName = QFileInfo(filePath).completeBaseName();
+ if(suffixNum > 0)
+ imageFileName += "-" + QString::number(suffixNum);
+ imageFileName += "." + QFileInfo(filePath).suffix();
+ return QDir(dicImagesDir).filePath(imageFileName);
+}
+
+int MainWindow::getCurEditorCursorPos()
+ {
+ const DictTableView* dictView = getCurDictView();
+ if(!dictView)
+ return -1;
+ return dictView->getEditorCursorPos();
+ }
+
+int MainWindow::getCurRow()
+{
+ return getCurDictView()->currentIndex().row();
+}
+
+int MainWindow::getCurColumn()
+{
+ return getCurDictView()->currentIndex().column();
+}
+
+Dictionary* MainWindow::getCurDict()
+{
+ return model->curDictionary();
+}
+
+void MainWindow::insertImageIntoCurEditor(int cursorPos, const QString& filePath)
+{
+ const DictTableView* dictView = getCurDictView();
+ const_cast<DictTableView*>(dictView)->startEditing(getCurRow(), getCurColumn());
+ dictView->insertImageIntoEditor(cursorPos, filePath);
+}
+
+void MainWindow::insertRecords()
+ {
+ pushToUnoStack(new InsertRecordsCmd(this));
+ }
+
+void MainWindow::removeRecords()
+ {
+ pushToUnoStack(new RemoveRecordsCmd(this));
+ }
+
+void MainWindow::pushToUnoStack(QUndoCommand* command)
+ {
+ getCurDictView()->dicTableModel()->undoStack()->push(command);
+ }
+
+void MainWindow::find()
+ {
+ findPanel->show();
+ findAgainAct->setEnabled(true);
+ }
+
+void MainWindow::findAgain()
+ {
+ findPanel->find();
+ }
+
+void MainWindow::createActions()
+{
+ createFileActions();
+ createEditActions();
+ createDictContextMenuActions();
+ createSelectionActions();
+ createToolsActions();
+ createSettingsActions();
+ createHelpActions();
+ initActions();
+}
+
+void MainWindow::createFileActions()
+{
+ newAct = new QAction(QIcon(":/images/filenew.png"), tr("&New"), this);
+ newAct->setShortcut(tr("Ctrl+N"));
+ connect(newAct, SIGNAL(triggered()), SLOT(newFile()));
+
+ loadAct = new QAction(QIcon(":/images/fileopen.png"), tr("&Open ..."), this);
+ loadAct->setShortcut(tr("Ctrl+O"));
+ connect(loadAct, SIGNAL(triggered()), SLOT(openFileWithDialog()));
+
+ openFlashcardsAct = new QAction(tr("Online dictionaries"), this);
+ connect(openFlashcardsAct, SIGNAL(triggered()), SLOT(openOnlineDictionaries()));
+
+ saveAct = new QAction(QIcon(":/images/filesave.png"), tr("&Save"), this);
+ saveAct->setShortcut(tr("Ctrl+S"));
+ connect(saveAct, SIGNAL(triggered()), SLOT(Save()));
+
+ saveAsAct = new QAction(QIcon(":/images/filesaveas.png"), tr("Save &as ..."), this);
+ connect(saveAsAct, SIGNAL(triggered()), SLOT(SaveAs()));
+
+ saveCopyAct = new QAction(tr("Save &copy ..."), this);
+ connect(saveCopyAct, SIGNAL(triggered()), SLOT(SaveCopy()));
+
+ importAct = new QAction(tr("&Import from CSV ..."), this);
+ connect(importAct, SIGNAL(triggered()), SLOT(importFromCsv()));
+
+ exportAct = new QAction(tr("&Export to CSV ..."), this);
+ connect(exportAct, SIGNAL(triggered()), SLOT(exportToCsv()));
+
+ removeTabAct = new QAction(QIcon(":/images/remove.png"),
+ tr("&Close dictionary"), this);
+ removeTabAct->setShortcut(tr("Ctrl+W"));
+ connect(removeTabAct, SIGNAL(triggered()), dictTabWidget, SLOT(closeTab()));
+
+ quitAct = new QAction(QIcon(":/images/exit.png"), tr("&Quit"), this);
+ quitAct->setShortcut(tr("Ctrl+Q"));
+ connect(quitAct, SIGNAL(triggered()), SLOT(close()));
+}
+
+void MainWindow::createEditActions()
+{
+ undoAct = dictTabWidget->undoGroup()->createUndoAction(this);
+ undoAct->setShortcut(tr("Ctrl+Z"));
+
+ redoAct = dictTabWidget->undoGroup()->createRedoAction(this);
+ redoAct->setShortcut(tr("Ctrl+Y"));
+
+ copyAct = new QAction(QIcon(":/images/editcopy.png"), tr("&Copy"), this);
+ copyAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+C") << QString("Ctrl+Insert"));
+ connect(copyAct, SIGNAL(triggered()), SLOT(copyEntries()));
+
+ cutAct = new QAction(QIcon(":/images/editcut.png"), tr("Cu&t"), this);
+ cutAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+X") << QString("Shift+Delete"));
+ connect(cutAct, SIGNAL(triggered()), SLOT(cutEntries()));
+
+ pasteAct = new QAction(QIcon(":/images/editpaste.png"), tr("&Paste"), this);
+ pasteAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+V") << QString("Shift+Insert"));
+ connect(pasteAct, SIGNAL(triggered()), SLOT(pasteEntries()));
+ connect(QApplication::clipboard(), SIGNAL(dataChanged()), SLOT(updatePasteAction()));
+
+ createAddImageAct();
+ createinsertRecordsAct();
+ createRemoveRecordsAct();
+
+ findAct = new QAction(QIcon(":/images/find.png"), tr("&Find..."), this);
+ findAct->setShortcut(tr("Ctrl+F"));
+ connect(findAct, SIGNAL(triggered()), SLOT(find()));
+
+ findAgainAct = new QAction(QIcon(":/images/next.png"), tr("Find &again"), this);
+ findAgainAct->setShortcut(QString("F3"));
+ connect(findAgainAct, SIGNAL(triggered()), SLOT(findAgain()));
+}
+
+void MainWindow::createAddImageAct()
+{
+ addImageAct = new QAction(QIcon(":/images/add-image.png"), tr("&Add image"), this);
+ addImageAct->setShortcut(tr("Ctrl+G"));
+ connect(addImageAct, SIGNAL(triggered()), SLOT(addImage()));
+ connect(dictTabWidget, SIGNAL(editingStateChanged()), SLOT(updateAddImageAction()));
+}
+
+void MainWindow::createinsertRecordsAct()
+{
+ insertRecordsAct = new QAction(QIcon(":/images/add.png"),
+ tr("&Insert record"), this);
+ insertRecordsAct->setShortcut(tr("Ctrl+I"));
+ connect(insertRecordsAct, SIGNAL(triggered()), SLOT(insertRecords()));
+}
+
+void MainWindow::createRemoveRecordsAct()
+{
+ removeRecordsAct = new QAction(QIcon(":/images/delete.png"),
+ tr("&Remove record"), this);
+ removeRecordsAct->setShortcut(Qt::Key_Delete);
+ connect(removeRecordsAct, SIGNAL(triggered()), SLOT(removeRecords()));
+}
+
+void MainWindow::createToolsActions()
+{
+ QSignalMapper* startStudySignalMapper = new QSignalMapper(this);
+ connect(startStudySignalMapper, SIGNAL(mapped(int)), SLOT(startStudy(int)));
+
+ wordDrillAct = new QAction(QIcon(":/images/word-drill.png"), tr("&Word drill"),
+ this);
+ wordDrillAct->setShortcut(QString("F5"));
+ connect(wordDrillAct, SIGNAL(triggered()), startStudySignalMapper, SLOT(map()));
+ startStudySignalMapper->setMapping(wordDrillAct, AppModel::WordDrill);
+
+ spacedRepetitionAct = new QAction(QIcon(":/images/spaced-rep.png"),
+ tr("&Spaced repetition"), this);
+ spacedRepetitionAct->setShortcut(QString("F6"));
+ connect(spacedRepetitionAct, SIGNAL(triggered()), startStudySignalMapper,
+ SLOT(map()));
+ startStudySignalMapper->setMapping(spacedRepetitionAct, AppModel::SpacedRepetition);
+
+ statisticsAct = new QAction(QIcon(":/images/statistics.png"), tr("S&tatistics"),
+ this);
+ statisticsAct->setShortcut(QString("F7"));
+ connect(statisticsAct, SIGNAL(triggered()), SLOT(showStatistics()));
+}
+
+void MainWindow::createSettingsActions()
+{
+ dictionaryOptionsAct = new QAction(QIcon(":/images/dic-options.png"),
+ tr("&Dictionary options"), this);
+ dictionaryOptionsAct->setShortcut(QString("Ctrl+1"));
+ connect(dictionaryOptionsAct, SIGNAL(triggered()), SLOT(openDictionaryOptions()));
+
+ fontColorSettingsAct = new QAction(QIcon(":/images/font-style.png"),
+ tr("&Font and color settings"), this);
+ fontColorSettingsAct->setShortcut(QString("Ctrl+2"));
+ connect(fontColorSettingsAct, SIGNAL(triggered()), SLOT(openFontColorSettings()));
+
+ studySettingsAct = new QAction(QIcon(":/images/study-settings.png"),
+ tr("&Study settings"), this);
+ studySettingsAct->setShortcut(QString("Ctrl+3"));
+ connect(studySettingsAct, SIGNAL(triggered()), SLOT(openStudySettings()));
+}
+
+void MainWindow::createHelpActions()
+{
+ helpAct = new QAction(tr("Help"), this);
+ helpAct->setShortcut(QString("F1"));
+ connect(helpAct, SIGNAL(triggered()), SLOT(help()));
+
+ aboutAct = new QAction(QIcon(":/images/freshmemory.png"), tr("About"), this);
+ connect(aboutAct, SIGNAL(triggered()), SLOT(about()));
+}
+
+void MainWindow::initActions()
+{
+ saveAct->setEnabled(false);
+ saveAsAct->setEnabled(false);
+ saveCopyAct->setEnabled(false);
+ exportAct->setEnabled(false);
+ removeTabAct->setEnabled(false);
+
+ cutAct->setEnabled(false);
+ copyAct->setEnabled(false);
+ pasteAct->setEnabled(false);
+ addImageAct->setEnabled(false);
+ insertRecordsAct->setEnabled(false);
+ removeRecordsAct->setEnabled(false);
+ findAct->setEnabled(false);
+ findAgainAct->setEnabled(false);
+
+ statisticsAct->setEnabled(false);
+ wordDrillAct->setEnabled(false);
+ spacedRepetitionAct->setEnabled(false);
+
+ dictionaryOptionsAct->setEnabled(false);
+}
+
+void MainWindow::createDictContextMenuActions()
+{
+ contextMenuActions << copyAct;
+ contextMenuActions << cutAct;
+ contextMenuActions << pasteAct;
+ contextMenuActions << insertRecordsAct;
+ contextMenuActions << removeRecordsAct;
+}
+
+void MainWindow::createSelectionActions()
+{
+ selectionActions << copyAct;
+ selectionActions << cutAct;
+ selectionActions << removeRecordsAct;
+}
+
+void MainWindow::createMenus()
+{
+ createFileMenu();
+
+ editMenu = menuBar()->addMenu( tr("&Edit") );
+ editMenu->addAction( undoAct );
+ editMenu->addAction( redoAct );
+ editMenu->addSeparator();
+
+ editMenu->addAction(cutAct);
+ editMenu->addAction(copyAct);
+ editMenu->addAction(pasteAct);
+ editMenu->addSeparator();
+
+ editMenu->addAction(addImageAct);
+ editMenu->addAction(insertRecordsAct);
+ editMenu->addAction(removeRecordsAct);
+ editMenu->addSeparator();
+ editMenu->addAction(findAct);
+ editMenu->addAction(findAgainAct);
+
+ viewMenu = menuBar()->addMenu( tr("&View") );
+
+ toolsMenu = menuBar()->addMenu( tr("&Tools") );
+ toolsMenu->addAction(wordDrillAct);
+ toolsMenu->addAction(spacedRepetitionAct);
+ toolsMenu->addAction(statisticsAct);
+
+ createOptionsMenu();
+
+ menuBar()->addSeparator();
+
+ helpMenu = menuBar()->addMenu( tr("&Help") );
+ helpMenu->addAction(helpAct);
+ helpMenu->addAction(aboutAct);
+}
+
+void MainWindow::createFileMenu()
+{
+ fileMenu = menuBar()->addMenu(tr("&File"));
+ fileMenu->addAction(newAct);
+ fileMenu->addAction(loadAct);
+ fileMenu->addAction(openFlashcardsAct);
+ fileMenu->addAction(saveAct);
+ fileMenu->addAction(saveAsAct);
+ fileMenu->addAction(saveCopyAct);
+ fileMenu->addAction(importAct);
+ fileMenu->addAction(exportAct);
+
+ createRecentFilesMenu();
+
+ fileMenu->addSeparator();
+ fileMenu->addAction(removeTabAct);
+ fileMenu->addAction(quitAct);
+}
+
+void MainWindow::createRecentFilesMenu()
+{
+ QMenu* recentFilesMenu = fileMenu->addMenu(tr("&Recent files"));
+ welcomeScreen->setRecentFilesMenu(recentFilesMenu);
+ recentFilesMan = new RecentFilesManager(this, recentFilesMenu);
+ connect(recentFilesMan, SIGNAL(recentFileTriggered(const QString&)),
+ SLOT(openFile(const QString&)));
+ connect(recentFilesMan, SIGNAL(addedFile()), welcomeScreen,
+ SLOT(updateRecentFilesButton()));
+}
+
+void MainWindow::createOptionsMenu()
+{
+ optionsMenu = menuBar()->addMenu(tr("&Options"));
+ optionsMenu->addAction( dictionaryOptionsAct );
+ optionsMenu->addAction( fontColorSettingsAct );
+ optionsMenu->addAction( studySettingsAct );
+
+ new LanguageMenu(optionsMenu);
+}
+
+void MainWindow::createToolBars()
+{
+ QToolBar* toolBar = addToolBar( tr("Main") );
+ toolBar->setObjectName("Main");
+ toolBar->addAction(newAct);
+ toolBar->addAction(loadAct);
+ toolBar->addAction(saveAct);
+ toolBar->addSeparator();
+ toolBar->addAction(findAct);
+ toolBar->addAction(wordDrillAct);
+ toolBar->addAction(spacedRepetitionAct);
+ toolBar->addAction(statisticsAct);
+ toolBar->addAction(addImageAct);
+}
+
+void MainWindow::createStatusBar()
+{
+ statusBar()->addPermanentWidget( totalRecordsLabel = new QLabel );
+ updateTotalRecordsLabel();
+}
+
+void MainWindow::createCentralWidget()
+ {
+ mainStackedWidget = new QStackedWidget;
+ welcomeScreen = new WelcomeScreen(this);
+ mainStackedWidget->addWidget(welcomeScreen);
+ mainStackedWidget->addWidget(createDictionaryView());
+ setCentralWidget(mainStackedWidget);
+ }
+
+QWidget* MainWindow::createDictionaryView()
+{
+ dictTabWidget = new DictionaryTabWidget(this);
+ connect(dictTabWidget, SIGNAL(currentChanged(int)), SLOT(curTabChanged(int)));
+ findPanel = new FindPanel(this);
+ findPanel->hide();
+
+ QVBoxLayout* dictionaryViewLt = new QVBoxLayout;
+ dictionaryViewLt->setContentsMargins(0, 0, 0, 0);
+ dictionaryViewLt->addWidget(dictTabWidget);
+ dictionaryViewLt->addWidget(findPanel);
+
+ QWidget* dictionaryView = new QWidget;
+ dictionaryView->setLayout(dictionaryViewLt);
+ return dictionaryView;
+}
+
+void MainWindow::curTabChanged(int tabIndex)
+{
+ updateMainStackedWidget(tabIndex);
+ model->setCurDictionary(tabIndex);
+ updateActions();
+ updateSelectionActions();
+ updateTotalRecordsLabel();
+ updatePackSelection(tabIndex);
+ updateCardPreview();
+}
+
+void MainWindow::updateMainStackedWidget(int tabIndex)
+{
+ if (tabIndex == -1)
+ mainStackedWidget->setCurrentIndex(0);
+ else
+ mainStackedWidget->setCurrentIndex(1);
+}
+
+void MainWindow::createDockWindows()
+ {
+ createPacksTreeDock();
+ createCardPreviewDock();
+ }
+
+void MainWindow::createPacksTreeDock()
+{
+ QDockWidget* packsDock = new QDockWidget(tr("Card packs"));
+ packsDock->setObjectName("Card-packs");
+ packsDock->setAllowedAreas(
+ Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+ packsTreeView = new QTreeView;
+ packsTreeView->setAllColumnsShowFocus(true);
+ packsTreeView->setRootIsDecorated(false);
+ packsTreeView->header()->setDefaultSectionSize(150);
+ PacksTreeModel* packsTreeModel = new PacksTreeModel(model);
+ packsTreeView->setModel(packsTreeModel);
+ packsDock->setWidget(packsTreeView);
+ addDockWidget(Qt::LeftDockWidgetArea, packsDock);
+ viewMenu->addAction(packsDock->toggleViewAction());
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(setCurDictTabByTreeIndex(const QModelIndex&)));
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateCardPreview()));
+ connect(packsTreeView, SIGNAL(activated(const QModelIndex&)),
+ SLOT(startSpacedRepetitionStudy()));
+}
+
+void MainWindow::createCardPreviewDock()
+{
+ cardPreview = new CardPreview(this);
+ addDockWidget(Qt::LeftDockWidgetArea, cardPreview);
+ viewMenu->addAction(cardPreview->toggleViewAction());
+}
+
+void MainWindow::updatePacksTreeView()
+{
+ static_cast<PacksTreeModel*>( packsTreeView->model() )->updateData();
+ packsTreeView->expandAll();
+ packsTreeView->resizeColumnToContents( 1 );
+ packsTreeView->resizeColumnToContents( 2 );
+ updatePackSelection();
+}
+
+void MainWindow::updateCardPreview()
+{
+ CardPack* pack = getCurCardPack();
+ Card* card = getCurCardFromPack(pack);
+ cardPreview->setContent(pack, card);
+}
+
+CardPack* MainWindow::getCurCardPack() const
+{
+ const Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return NULL;
+ int packId = packsTreeView->currentIndex().row();
+ return dict->cardPack(packId);
+}
+
+const DicRecord* MainWindow::getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const
+{
+ if(!dict || !dictView)
+ return NULL;
+ return dict->getRecord(dictView->currentIndex().row());
+}
+
+const DicRecord* MainWindow::getCurDictRecord() const
+{
+ return getCurDictRecord(model->curDictionary(), getCurDictView());
+}
+
+Card* MainWindow::getCurCardFromPack(CardPack* pack) const
+{
+ const DicRecord* record = getCurDictRecord();
+ if(!record)
+ return NULL;
+ return pack->getCard(record->getPreviewQuestionForPack(pack));
+}
+
+/// Proxy-view-aware copying
+void MainWindow::copyEntries()
+ {
+ const DictTableView* tableView = getCurDictView(); // May be proxy view
+ if( !tableView )
+ return;
+ QModelIndexList selectedIndexes = tableView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( tableView->model() );
+ if( proxyModel )
+ {
+ QModelIndexList srcIndexes;
+ foreach( QModelIndex index, selectedIndexes )
+ {
+ QModelIndex srcIndex = proxyModel->mapToSource( index );
+ srcIndexes << srcIndex;
+ }
+ selectedIndexes = srcIndexes;
+ }
+ Dictionary* dict = model->curDictionary();
+ QList<DicRecord*> entries;
+ foreach( QModelIndex index, selectedIndexes )
+ entries << const_cast<DicRecord*>(dict->getRecord( index.row() ));
+
+ CsvExportData params;
+ params.commentChar = '#';
+ params.fieldSeparators = "& ";
+ params.quoteAllFields = false;
+ params.textDelimiter = '"';
+ params.writeColumnNames = true;
+ DicCsvWriter csvWriter( entries );
+ QString copiedStr = csvWriter.toCsvString( params );
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText( copiedStr );
+ }
+
+void MainWindow::cutEntries()
+ {
+ copyEntries();
+ removeRecords();
+ }
+
+void MainWindow::pasteEntries()
+ {
+ // Get text from clipboard
+ QClipboard *clipboard = QApplication::clipboard();
+ QString pastedStr = clipboard->text();
+ if( pastedStr.isEmpty() )
+ return;
+
+ const DictTableView* dictView = getCurDictView(); // May be proxy view
+ Q_ASSERT( dictView );
+ DictTableModel* dictModel = dictView->dicTableModel(); // Always original model
+ Q_ASSERT( dictModel );
+
+ // Parse records to be pasted
+ DicCsvReader csvReader;
+ CsvImportData params;
+ params.firstLineIsHeader = true;
+ params.commentChar = '#';
+ params.fieldSeparationMode = EFieldSeparatorExactString;
+ params.fieldSeparators = "& ";
+ params.fromLine = 1;
+ params.textDelimiter = '"';
+ params.colsToImport = 0; // All
+ QList<DicRecord*> records = csvReader.readEntries( pastedStr, params );
+ if( records.empty() )
+ return;
+ QStringList pastedFieldNames = csvReader.fieldNames();
+
+ // Check for new pasted fields. Ask user what to do with them.
+ QStringList dicFieldNames = model->curDictionary()->fieldNames();
+ QStringList newFieldNames;
+ foreach( QString name, pastedFieldNames )
+ if( !dicFieldNames.contains( name ) )
+ newFieldNames << name;
+ if( !newFieldNames.isEmpty() )
+ {
+ QMessageBox msgBox;
+ msgBox.setWindowTitle( windowTitle() );
+ bool existingFields = pastedFieldNames.size() - newFieldNames.size() > 0;
+ msgBox.setText( tr("The pasted records contain %n new field(s)", "", newFieldNames.size()) + ":\n" + newFieldNames.join(", ") + "." );
+ msgBox.setInformativeText(tr("Do you want to add new fields to this dictionary?"));
+ msgBox.setIcon( QMessageBox::Question );
+ QPushButton* addFieldsBtn = msgBox.addButton( tr("Add new fields"), QMessageBox::YesRole );
+ QPushButton* pasteExistingBtn = msgBox.addButton( tr("Paste only existing fields"), QMessageBox::NoRole );
+ pasteExistingBtn->setEnabled( existingFields );
+ msgBox.addButton( QMessageBox::Cancel );
+ msgBox.setDefaultButton( addFieldsBtn );
+ int res = msgBox.exec();
+ QAbstractButton* clickedBtn = msgBox.clickedButton();
+ if( clickedBtn == addFieldsBtn )
+ dictModel->addFields( newFieldNames );
+ else if( res == QMessageBox::Cancel ) // do not paste records
+ return;
+ // If paste only existing fields, don't make any changes here, just continue.
+ }
+
+ QUndoCommand* command = new PasteRecordsCmd( records, this ); // Takes ownership of records
+ dictModel->undoStack()->push( command );
+ }
+
+void MainWindow::readSettings()
+{
+ loadGeneralSettings();
+ StudySettings::inst()->load();
+}
+
+void MainWindow::loadGeneralSettings()
+{
+ QSettings settings;
+ move(settings.value("main-pos", QPoint(100, 100)).toPoint());
+ resize(settings.value("main-size", QSize(600, 500)).toSize());
+ if (settings.value("main-maximized", false).toBool())
+ showMaximized();
+
+ restoreState(settings.value("main-state").toByteArray(), 0);
+ QStringList recentFiles = settings.value("main-recent-files").toStringList();
+ recentFilesMan->createRecentFileActs(recentFiles);
+ workPath = settings.value("work-path",
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).
+ toString();
+}
+
+void MainWindow::writeSettings()
+{
+ saveGeneralSettings();
+}
+
+void MainWindow::saveGeneralSettings()
+{
+ QSettings settings;
+ settings.setValue("main-maximized", isMaximized());
+ if(!isMaximized())
+ {
+ settings.setValue("main-pos", pos());
+ settings.setValue("main-size", size());
+ }
+ settings.setValue("main-state", saveState(0));
+ settings.setValue("main-recent-files", recentFilesMan->getFiles());
+ settings.setValue("work-path", workPath);
+ saveSession();
+}
+
+void MainWindow::saveSession()
+{
+ QSettings settings;
+ QStringList sessionFiles;
+ for(int i = 0; i < dictTabWidget->count(); i++)
+ {
+ const Dictionary* dict = model->dictionary(i);
+ sessionFiles << dict->getFilePath();
+ }
+ settings.setValue("session", sessionFiles);
+ settings.setValue("session-cur-tab", model->getCurDictIndex());
+}
+
+void MainWindow::startStudy(int studyType)
+ {
+ int packIx = packsTreeView->currentIndex().row();
+
+ if(studyWindow)
+ {
+ if((int)studyWindow->getStudyType() == studyType)
+ {
+ studyWindow->showNormal();
+ studyWindow->raise();
+ studyWindow->activateWindow();
+ return;
+ }
+ else
+ {
+ disconnect(studyWindow, SIGNAL(destroyed()), this, SLOT(activate()));
+ studyWindow->close();
+ }
+ }
+
+ IStudyModel* studyModel = model->createStudyModel(studyType, packIx);
+ if(!studyModel)
+ {
+ QMessageBox::critical(this, Strings::errorTitle(),
+ tr("The study cannot be started.", "First part of error message") + "\n" +
+ model->getErrorMessage());
+ return;
+ }
+
+ if( studyType == AppModel::SpacedRepetition )
+ connect(studyModel, SIGNAL(nextCardSelected()), SLOT(updatePacksTreeView()));
+
+ // Create study window
+ switch( studyType )
+ {
+ case AppModel::WordDrill:
+ studyWindow = new WordDrillWindow(dynamic_cast<WordDrillModel*>(studyModel), this );
+ break;
+ case AppModel::SpacedRepetition:
+ studyWindow = new SpacedRepetitionWindow(dynamic_cast<SpacedRepetitionModel*>(studyModel), this );
+ break;
+ }
+ connect(studyWindow, SIGNAL(destroyed()), SLOT(activate()));
+ studyWindow->show();
+ }
+
+void MainWindow::startSpacedRepetitionStudy()
+ {
+ startStudy( AppModel::SpacedRepetition );
+ }
+
+void MainWindow::goToDictionaryRecord(const Dictionary* aDictionary, int aRecordRow )
+ {
+ int dicIx = model->indexOfDictionary( const_cast<Dictionary*>( aDictionary ) );
+ if( dicIx < 0 )
+ return;
+ show();
+ raise();
+ activateWindow();
+ dictTabWidget->goToDictionaryRecord( dicIx, aRecordRow );
+ }
+
+void MainWindow::openDictionaryOptions()
+{
+ Dictionary* curDict = model->curDictionary();
+ if( !curDict )
+ return;
+ DictionaryOptionsDialog dicOptions( curDict, this );
+ int res = dicOptions.exec();
+ if (res == QDialog::Accepted)
+ {
+ curDict->setDictConfig( &dicOptions.m_dict );
+ getCurDictView()->dicTableModel()->resetData();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::openFontColorSettings()
+{
+ FontColorSettingsDialog dialog( this );
+ int res = dialog.exec();
+ if (res == QDialog::Accepted)
+ {
+ *(FieldStyleFactory::inst()) = *dialog.styleFactory();
+ FieldStyleFactory::inst()->save();
+ }
+}
+
+void MainWindow::openStudySettings()
+{
+ StudySettingsDialog dialog(this);
+ if(dialog.exec() == QDialog::Accepted)
+ {
+ *(StudySettings::inst()) = dialog.getSettings();
+ StudySettings::inst()->save();
+ }
+}
+
+void MainWindow::setCurDictTab(int index)
+{
+ dictTabWidget->setCurrentIndex(index);
+}
+
+void MainWindow::setCurDictTabByTreeIndex( const QModelIndex& aIndex )
+{
+ if( !aIndex.isValid() )
+ return;
+ dictTabWidget->setCurrentIndex( static_cast<TreeItem*>( aIndex.internalPointer() )->topParentRow() );
+}
+
+// Set the pack selection to the correct dictionary
+void MainWindow::updatePackSelection( int aDic )
+{
+ if( !model->curDictionary() )
+ return;
+ if( aDic == -1 ) // default value
+ aDic = model->curDictionary()->row();
+ QModelIndex curIx = packsTreeView->currentIndex();
+ if( curIx.isValid() && static_cast<TreeItem*>( curIx.internalPointer() )->topParentRow() == aDic )
+ return;
+ QAbstractItemModel* packsTreeModel = packsTreeView->model();
+ QModelIndex dicIx = packsTreeModel->index( aDic, 0, QModelIndex() );
+ int curPackIx;
+ if( !studyWindow.isNull() )
+ curPackIx = model->curCardPackIx();
+ else
+ curPackIx = 0; // first pack
+ QModelIndex packIndex = packsTreeModel->index( curPackIx, 0, dicIx );
+ packsTreeView->selectionModel()->setCurrentIndex( packIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
+}
+
+void MainWindow::showStatistics()
+{
+ StatisticsView(getCurDict()).exec();
+}
diff --git a/src/main-view/MainWindow.h b/src/main-view/MainWindow.h
new file mode 100644
index 0000000..5165118
--- /dev/null
+++ b/src/main-view/MainWindow.h
@@ -0,0 +1,218 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QtWidgets>
+
+class AppModel;
+class DictTableView;
+class Dictionary;
+class IStudyWindow;
+class FindPanel;
+class CardPreview;
+class RecentFilesManager;
+class DicRecord;
+class Card;
+class CardPack;
+class WelcomeScreen;
+
+#include "UndoCommands.h"
+#include "DictionaryTabWidget.h"
+
+class MainWindow: public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(AppModel* model);
+ ~MainWindow();
+
+ const DictTableView* getCurDictView() const;
+ QList<QAction*> getContextMenuActions() const { return contextMenuActions; }
+
+ void showContinueSearch() { dictTabWidget->showContinueSearch(); }
+ void goToDictionaryRecord( const Dictionary* aDictionary, int aRecordRow );
+
+public slots:
+ void openFile(const QString& filePath);
+
+private:
+ void init();
+ void addDictTab(Dictionary* dict);
+
+ void createActions();
+ void createFileActions();
+ void createEditActions();
+ void createAddImageAct();
+ void createinsertRecordsAct();
+ void createInsertRecordsAfterAct();
+ void createRemoveRecordsAct();
+ void createDictContextMenuActions();
+ void createSelectionActions();
+ void createToolsActions();
+ void createSettingsActions();
+ void createHelpActions();
+ void initActions();
+
+ void createMenus();
+ void createToolBars();
+ void createStatusBar();
+
+ void createCentralWidget();
+ void updateMainStackedWidget(int tabIndex);
+ QWidget* createDictionaryView();
+
+ void createDockWindows();
+ void createPacksTreeDock();
+ void createCardPreviewDock();
+
+ void readSettings();
+ void writeSettings();
+ bool doSave( const QString& aFilePath, bool aChangeFilePath = true );
+ bool proposeToSave();
+ void openSession();
+ void closeEvent(QCloseEvent *event);
+ void saveGeneralSettings();
+ void saveSession();
+ void loadGeneralSettings();
+
+ void updatePackSelection( int aDic = -1 );
+ Dictionary* getCurDict();
+ QString selectAddImageFile();
+ void checkAddImagePath();
+ QString copyImageFileToImagesDir(const QString& filePath);
+ QString createNewImageFilePath(const QString& dicImagesDir, const QString& filePath);
+ QString createImagesDirFilePath(const QString& dicImagesDir, const QString& filePath,
+ int suffixNum);
+ int getCurEditorCursorPos();
+ int getCurRow();
+ int getCurColumn();
+ void insertImageIntoCurEditor(int cursorPos, const QString& filePath);
+
+ void createFileMenu();
+ void createOptionsMenu();
+ void createRecentFilesMenu();
+
+ CardPack* getCurCardPack() const;
+ const DicRecord* getCurDictRecord() const;
+ const DicRecord* getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const;
+ Card* getCurCardFromPack(CardPack* pack) const;
+ bool isDictOpened(int index);
+ void setCurDictionary(int index);
+ void openFileWithDialog(const QString& dirPath);
+ bool loadFile(const QString& filePath);
+ void showOpenFileError();
+ void updateAfterOpenedDictionary(const QString& filePath);
+
+public slots:
+ void updateDictTab();
+ void updateActions();
+ void updatePasteAction();
+ void updateSelectionActions();
+ bool proposeToSave(int i);
+
+private slots:
+ void activate();
+ void curTabChanged(int tabIndex);
+ void updatePacksTreeView();
+ void updateCardPreview();
+ void updateAddImageAction();
+ void newFile();
+ void openFileWithDialog();
+ void openOnlineDictionaries();
+ bool Save();
+ bool saveStudy();
+ bool SaveAs( bool aChangeFilePath = true );
+ void SaveCopy();
+ void importFromCsv();
+ void exportToCsv();
+
+ void copyEntries();
+ void cutEntries();
+ void pasteEntries();
+ void addImage();
+ void insertRecords();
+ void removeRecords();
+ void pushToUnoStack(QUndoCommand* command);
+ void find();
+ void findAgain();
+ void openDictionaryOptions();
+ void openFontColorSettings();
+ void openStudySettings();
+
+ void startStudy(int aStudyType);
+ void startSpacedRepetitionStudy();
+ void help();
+ void about();
+ void updateTotalRecordsLabel();
+ void setCurDictTab(int index);
+ void setCurDictTabByTreeIndex(const QModelIndex& aIndex);
+ void showStatistics();
+
+ void saveStudyWithDelay(bool studyModified);
+
+private:
+ static const int AutoSaveStudyInterval = 3 * 60 * 1000; // ms
+
+private:
+ QString workPath;
+ QString addImagePath;
+ AppModel* model; // not own
+
+ // UI elements
+ QStackedWidget* mainStackedWidget;
+ DictionaryTabWidget* dictTabWidget;
+ WelcomeScreen* welcomeScreen;
+ QLabel* totalRecordsLabel;
+ QPointer<IStudyWindow> studyWindow;
+ FindPanel* findPanel;
+ QTreeView* packsTreeView;
+ CardPreview* cardPreview;
+
+ QMenu* fileMenu;
+ QMenu* editMenu;
+ QMenu* viewMenu;
+ QMenu* toolsMenu;
+ QMenu* optionsMenu;
+ QMenu* helpMenu;
+
+ RecentFilesManager* recentFilesMan;
+
+ // Actions
+ QAction* newAct;
+ QAction* loadAct;
+ QAction* openFlashcardsAct;
+ QAction* saveAct;
+ QAction* saveAsAct;
+ QAction* saveCopyAct;
+ QAction* importAct;
+ QAction* exportAct;
+ QAction* removeTabAct;
+ QAction* quitAct;
+
+ QAction* undoAct;
+ QAction* redoAct;
+ QAction* cutAct;
+ QAction* copyAct;
+ QAction* pasteAct;
+ QAction* addImageAct;
+ QAction* insertRecordsAct;
+ QAction* removeRecordsAct;
+ QAction* findAgainAct;
+ QAction* findAct;
+ QList<QAction*> contextMenuActions;
+ QList<QAction*> selectionActions;
+
+ QAction* wordDrillAct;
+ QAction* spacedRepetitionAct;
+ QAction* statisticsAct;
+
+ QAction* dictionaryOptionsAct;
+ QAction* fontColorSettingsAct;
+ QAction* studySettingsAct;
+
+ QAction* helpAct;
+ QAction* aboutAct;
+};
+
+#endif
diff --git a/src/main-view/PacksTreeModel.cpp b/src/main-view/PacksTreeModel.cpp
new file mode 100644
index 0000000..55b8c34
--- /dev/null
+++ b/src/main-view/PacksTreeModel.cpp
@@ -0,0 +1,93 @@
+#include "PacksTreeModel.h"
+#include "AppModel.h"
+#include "../dictionary/TreeItem.h"
+
+PacksTreeModel::PacksTreeModel( AppModel* aAppModel, QObject* aParent ):
+ QAbstractItemModel( aParent ), m_appModel( aAppModel )
+ {
+ }
+
+QVariant PacksTreeModel::data(const QModelIndex &index, int role) const
+ {
+ if( !index.isValid() )
+ return QVariant();
+ if( role != Qt::DisplayRole )
+ return QVariant();
+ TreeItem* item = static_cast<TreeItem*>( index.internalPointer() );
+ return item->data( index.column() );
+ }
+
+Qt::ItemFlags PacksTreeModel::flags(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return 0;
+ if( !index.parent().isValid() ) // First level item = dictionary
+ return Qt::ItemIsEnabled;
+ else // pack
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ }
+
+QVariant PacksTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
+ {
+ if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
+ switch( section )
+ {
+ case 0: return tr("Card pack"); break;
+ case 1: return tr("Sched"); break;
+ case 2: return tr("New"); break;
+ }
+ return QVariant();
+ }
+
+QModelIndex PacksTreeModel::index(int row, int column, const QModelIndex &parent) const
+ {
+ if( !hasIndex(row, column, parent) )
+ return QModelIndex();
+ if( !parent.isValid() ) // First level item = dictionary
+ {
+ Dictionary* dic = m_appModel->dictionary( row );
+ if( dic )
+ return createIndex( row, column, dic );
+ else
+ return QModelIndex();
+ }
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ const TreeItem* childItem = parentItem->child( row );
+ if( childItem )
+ return createIndex( row, column, (void*)childItem );
+ else
+ return QModelIndex();
+ }
+
+QModelIndex PacksTreeModel::parent(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return QModelIndex();
+ TreeItem* childItem = static_cast<TreeItem*>( index.internalPointer() );
+ if( !childItem )
+ return QModelIndex();
+ const TreeItem* parentItem = childItem->parent();
+
+ if( parentItem )
+ return createIndex( parentItem->row(), 0, (void*)parentItem );
+ else
+ return QModelIndex(); // The root item
+ }
+
+int PacksTreeModel::rowCount(const QModelIndex &parent) const
+ {
+ if( parent.column() > 0 ) // Only the first column may have children
+ return 0;
+ if( !parent.isValid() ) // Root item
+ return m_appModel->dictionariesNum();
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ return parentItem->childCount();
+ }
+
+void PacksTreeModel::updateData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+
diff --git a/src/main-view/PacksTreeModel.h b/src/main-view/PacksTreeModel.h
new file mode 100644
index 0000000..f04036d
--- /dev/null
+++ b/src/main-view/PacksTreeModel.h
@@ -0,0 +1,32 @@
+#ifndef PACKSTREEMODEL_H
+#define PACKSTREEMODEL_H
+
+#include <QAbstractItemModel>
+
+#include "../dictionary/CardPack.h"
+
+class AppModel;
+
+class PacksTreeModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ PacksTreeModel( AppModel* aAppModel, QObject* aParent = 0);
+
+ QVariant data( const QModelIndex &index, int role ) const;
+ Qt::ItemFlags flags( const QModelIndex &index ) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+ QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const;
+ QModelIndex parent( const QModelIndex &index ) const;
+ int rowCount( const QModelIndex &parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex &/*parent*/ = QModelIndex() ) const { return 3; }
+
+public slots:
+ void updateData();
+
+private:
+ AppModel* m_appModel;
+};
+
+#endif // PACKSTREEMODEL_H
diff --git a/src/main-view/RecentFilesManager.cpp b/src/main-view/RecentFilesManager.cpp
new file mode 100644
index 0000000..d554eb4
--- /dev/null
+++ b/src/main-view/RecentFilesManager.cpp
@@ -0,0 +1,99 @@
+#include "RecentFilesManager.h"
+
+RecentFilesManager::RecentFilesManager(QObject* parent, QMenu* recentFilesMenu):
+ QObject(parent), recentFilesMenu(recentFilesMenu)
+{
+}
+
+void RecentFilesManager::createRecentFileActs(const QStringList& recentFiles)
+{
+ if(recentFiles.isEmpty())
+ {
+ recentFilesMenu->setEnabled(false);
+ return;
+ }
+ int recentFilesNum = recentFiles.size();
+ if(recentFilesNum > MaxRecentFiles)
+ recentFilesNum = MaxRecentFiles;
+ for(int i = 0; i < recentFilesNum; i++)
+ recentFilesMenu->addAction(createRecentFileAction(recentFiles[i]));
+ updateActionTexts();
+}
+
+QAction* RecentFilesManager::createRecentFileAction(const QString& filePath)
+{
+ QAction* action = new QAction(this);
+ action->setData(QDir::fromNativeSeparators(filePath));
+ connect(action, SIGNAL(triggered()), this, SLOT(triggerRecentFile()));
+ return action;
+}
+
+void RecentFilesManager::updateActionTexts()
+{
+ for(int i = 0; i < recentFilesMenu->actions().size(); i++)
+ {
+ QAction* action = recentFilesMenu->actions()[i];
+ QString filePath = getActionFile(action);
+ QString fileName = getShortFileName(filePath);
+ QString fileDir = QDir::toNativeSeparators(getShortDirPath(filePath));
+ QString text = QString("&%1 %2 (%3)").arg((i + 1) % 10)
+ .arg(fileName).arg(fileDir);
+ action->setText(text);
+ }
+}
+
+QString RecentFilesManager::getActionFile(QAction* action)
+{
+ return action->data().toString();
+}
+
+QString RecentFilesManager::getLastUsedFilePath() const
+{
+ if(recentFilesMenu->actions().isEmpty())
+ return QString();
+ return getActionFile(recentFilesMenu->actions().first());
+}
+
+QString RecentFilesManager::getShortDirPath(const QString& filePath) const
+{
+ QString path = QFileInfo(filePath).absolutePath();
+ QFontMetrics metrics(recentFilesMenu->font());
+ return metrics.elidedText(path, Qt::ElideMiddle, MaxPathLength);
+}
+
+void RecentFilesManager::triggerRecentFile()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ if(action)
+ emit recentFileTriggered(getActionFile(action));
+}
+
+void RecentFilesManager::addFile( const QString& filePath)
+{
+ addRecentFileAction(filePath);
+ updateActionTexts();
+ emit addedFile();
+}
+
+void RecentFilesManager::addRecentFileAction(const QString& filePath)
+{
+ recentFilesMenu->setEnabled(true);
+ foreach(QAction* action, recentFilesMenu->actions())
+ if(action->data() == filePath)
+ recentFilesMenu->removeAction(action);
+ QAction* newAction = createRecentFileAction(filePath);
+ if(!recentFilesMenu->actions().isEmpty())
+ recentFilesMenu->insertAction(recentFilesMenu->actions().first(), newAction);
+ else
+ recentFilesMenu->addAction(newAction);
+ if(recentFilesMenu->actions().size() > MaxRecentFiles)
+ recentFilesMenu->removeAction(recentFilesMenu->actions().last());
+}
+
+QStringList RecentFilesManager::getFiles() const
+{
+ QStringList list;
+ foreach(QAction* action, recentFilesMenu->actions())
+ list << getActionFile(action);
+ return list;
+}
diff --git a/src/main-view/RecentFilesManager.h b/src/main-view/RecentFilesManager.h
new file mode 100644
index 0000000..7988a0e
--- /dev/null
+++ b/src/main-view/RecentFilesManager.h
@@ -0,0 +1,43 @@
+#ifndef RECENT_FILES_MANAGER_H
+#define RECENT_FILES_MANAGER_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class RecentFilesManager: public QObject
+{
+ Q_OBJECT
+
+public:
+ RecentFilesManager(QObject* parent, QMenu* recentFilesMenu);
+ void createRecentFileActs(const QStringList& recentFiles);
+ void addFile( const QString& filePath);
+ QString getLastUsedFilePath() const;
+ QStringList getFiles() const;
+
+private:
+ static QString getShortFileName(const QString& filePath)
+ { return QFileInfo(filePath).fileName(); }
+ static QString getActionFile(QAction* action);
+
+private:
+ QAction* createRecentFileAction(const QString& filePath);
+ void updateActionTexts();
+ QString getShortDirPath(const QString& filePath) const;
+ void addRecentFileAction(const QString& filePath);
+
+private slots:
+ void triggerRecentFile();
+
+signals:
+ void recentFileTriggered(const QString& filePath);
+ void addedFile();
+
+private:
+ static const int MaxRecentFiles = 10;
+ static const int MaxPathLength = 300;
+
+private:
+ QMenu* recentFilesMenu;
+};
+#endif
diff --git a/src/main-view/RecordEditor.cpp b/src/main-view/RecordEditor.cpp
new file mode 100644
index 0000000..3f7a96c
--- /dev/null
+++ b/src/main-view/RecordEditor.cpp
@@ -0,0 +1,122 @@
+#include "RecordEditor.h"
+
+#include <QtDebug>
+
+const float RecordEditor::EditorWidthIncrease = 1.5;
+
+RecordEditor::RecordEditor(QWidget* parent, const QRect& cellRect) :
+ QTextEdit(parent), cellRect(cellRect),
+ enabledSizeUpdates(true)
+{
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ connect( this, SIGNAL(textChanged()), SLOT(updateEditor()) );
+}
+
+void RecordEditor::startDrawing()
+{
+ clear();
+ enabledSizeUpdates = false;
+}
+
+void RecordEditor::endDrawing()
+{
+ enabledSizeUpdates = true;
+ updateEditor();
+}
+
+void RecordEditor::drawText(const QString& text)
+{
+ textCursor().insertText(text);
+}
+
+void RecordEditor::drawImage(const QString& filePath)
+{
+ QTextImageFormat format;
+ format.setVerticalAlignment(QTextCharFormat::AlignBaseline);
+ format.setName(filePath);
+ format.setHeight(ThumbnailSize);
+ textCursor().insertImage(format);
+}
+
+void RecordEditor::insertImage(int cursorPos, const QString& filePath)
+{
+ moveCursor(cursorPos);
+ drawImage(filePath);
+}
+
+void RecordEditor::moveCursor(int cursorPos)
+{
+ QTextCursor cursor = textCursor();
+ cursor.setPosition(cursorPos);
+ setTextCursor(cursor);
+}
+
+QString RecordEditor::getText() const
+{
+ QString text;
+ QTextBlock block = document()->begin();
+ while(block.isValid())
+ {
+ for(QTextBlock::iterator it = block.begin(); !it.atEnd(); it++)
+ {
+ QTextCharFormat format = it.fragment().charFormat();
+ if(format.isImageFormat())
+ text += QString("<img src=\"") + format.toImageFormat().name() + "\">";
+ else
+ text += it.fragment().text();
+ }
+ block = block.next();
+ }
+ return text;
+}
+
+QSize RecordEditor::sizeHint() const
+{
+ int width = getEditorWidth();
+ return QSize(width, getEditorHeight());
+}
+
+int RecordEditor::getEditorWidth() const
+{
+ int horMargins = contentsMargins().left() * 2;
+ int textWidth = cellRect.width() - horMargins;
+ document()->setTextWidth(textWidth);
+ if(textWrapped() && textWidth < MinWidth)
+ {
+ textWidth = MinWidth;
+ document()->setTextWidth(textWidth);
+ }
+ return textWidth + horMargins;
+}
+
+bool RecordEditor::textWrapped() const
+{
+ return document()->size().height() > cellRect.height();
+}
+
+int RecordEditor::getEditorHeight() const
+{
+ return document()->size().height() + contentsMargins().top() * 2;
+}
+
+void RecordEditor::updateEditor()
+{
+ if(!enabledSizeUpdates)
+ return;
+ adjustSize();
+ updatePos();
+}
+
+void RecordEditor::updatePos()
+{
+ QRect editorRect = cellRect;
+ editorRect.setSize(size());
+ QSize viewportSize = qobject_cast<QWidget*>(parent())->size();
+ if(editorRect.right() > viewportSize.width())
+ editorRect.moveRight(viewportSize.width());
+ if(editorRect.bottom() > viewportSize.height())
+ editorRect.moveBottom(viewportSize.height());
+ move(editorRect.topLeft());
+}
diff --git a/src/main-view/RecordEditor.h b/src/main-view/RecordEditor.h
new file mode 100644
index 0000000..40b5dc5
--- /dev/null
+++ b/src/main-view/RecordEditor.h
@@ -0,0 +1,46 @@
+#ifndef RECORD_EDITOR_H
+#define RECORD_EDITOR_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class RecordEditor: public QTextEdit, public FieldContentPainter
+{
+Q_OBJECT
+
+private:
+ static const int ThumbnailSize = 25;
+ static const int MinWidth = 150;
+ static const int MaxWidth = 400;
+ static const float EditorWidthIncrease;
+
+public:
+ RecordEditor(QWidget* parent, const QRect& cellRect);
+ QRect getCellRect() const { return cellRect; }
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+ void insertImage(int cursorPos, const QString& filePath);
+ QString getText() const;
+
+protected:
+ QSize sizeHint() const;
+
+private:
+ int getEditorWidth() const;
+ int getEditorHeight() const;
+ void updatePos();
+ bool textWrapped() const;
+ void moveCursor(int cursorPos);
+
+public slots:
+ void updateEditor();
+
+private:
+ QRect cellRect;
+ bool enabledSizeUpdates;
+};
+#endif
+
diff --git a/src/main-view/UndoCommands.cpp b/src/main-view/UndoCommands.cpp
new file mode 100644
index 0000000..349b2a7
--- /dev/null
+++ b/src/main-view/UndoCommands.cpp
@@ -0,0 +1,322 @@
+#include "UndoCommands.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../main-view/MainWindow.h"
+#include "../main-view/CardFilterModel.h"
+
+UndoRecordCmd::UndoRecordCmd( const MainWindow* aMainWin ):
+ m_mainWindow( aMainWin )
+ {
+ }
+
+const DictTableView* UndoRecordCmd::getCurView()
+ {
+ if( !m_mainWindow )
+ return NULL;
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ return dictView;
+ }
+
+CardFilterModel* UndoRecordCmd::getProxyModel( const DictTableView* aCurDictView )
+ {
+ CardFilterModel* proxyModel = qobject_cast<CardFilterModel*>( aCurDictView->model() );
+ return proxyModel;
+ }
+
+void UndoRecordCmd::insertRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = getCurView();
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+
+ // Insert rows by positions in the direct order
+ foreach( int row, aRowNumbers )
+ {
+ if( proxyModel )
+ proxyModel->addFilterRow( row );
+ m_dictModel->insertRows( row, 1 );
+ }
+ }
+
+void UndoRecordCmd::setRecords( QMap<int, DicRecord*> aRecords )
+ {
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Init rows by positions in the direct order
+ foreach( int row, aRecords.keys() )
+ {
+ QModelIndex index = m_dictModel->index( row, 0 );
+ DicRecord* record = aRecords.value( row );
+ m_dictModel->setData( index, QVariant::fromValue( *record ), DictTableModel::DicRecordRole );
+ }
+ dict->enableRecordUpdates();
+ }
+
+void UndoRecordCmd::removeRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Remove records by ranges from back
+ QListIterator<int> i( aRowNumbers );
+ i.toBack();
+ while( i.hasPrevious() )
+ {
+ int pos = i.previous();
+ if( proxyModel )
+ proxyModel->removeFilterRow( pos );
+ m_dictModel->removeRows( pos, 1 );
+ }
+ dict->enableRecordUpdates();
+ }
+
+
+/** Selects rows in the list.
+ * Sets the first row in the list as current.
+ */
+void UndoRecordCmd::selectRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ int topRow = aRowNumbers.first();
+ if( topRow > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ topRow = m_dictModel->rowCount() - 1;
+ QModelIndex topIndex = m_dictModel->index( topRow, 0, QModelIndex() );
+ QItemSelectionModel* selectionModel = dictView->selectionModel();
+ selectionModel->clear();
+ foreach( int row, aRowNumbers )
+ {
+ if( row > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ row = m_dictModel->rowCount() - 1;
+ QModelIndex index = m_dictModel->index( row, 0, QModelIndex() );
+ if( proxyModel )
+ {
+ index = proxyModel->mapFromSource( index );
+ if( !index.isValid() )
+ continue;
+ }
+ selectionModel->setCurrentIndex( index, QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ }
+ if( proxyModel )
+ topIndex = proxyModel->mapFromSource( topIndex );
+ if( topIndex.isValid() )
+ selectionModel->setCurrentIndex( topIndex, QItemSelectionModel::Current );
+ }
+
+void UndoRecordCmd::selectRow( int aRow )
+ {
+ QList<int> list;
+ list << aRow;
+ selectRows( list );
+ }
+
+//==============================================================================================================
+
+InsertRecordsCmd::InsertRecordsCmd( const MainWindow* aMainWin):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // If no selection, insert 1 row in the end
+ int pos = m_dictModel->rowCount();
+ int rowsNum = 1;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ if( !selectedRows.isEmpty() )
+ // Insert number of rows equal to the selected rows
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ qSort( selectedRows ); // to find the top row
+ pos = selectedRows.first().row();
+ rowsNum = selectedRows.size();
+ }
+ for( int r = pos; r < pos + rowsNum; r++ )
+ m_rowNumbers << r;
+
+ updateText();
+ }
+
+void InsertRecordsCmd::updateText()
+ {
+ setText(QObject::tr("Insert %n record(s)", "Undo action of inserting records", m_rowNumbers.size() ));
+ }
+
+void InsertRecordsCmd::redo()
+ {
+ insertRows( m_rowNumbers );
+ selectRows( m_rowNumbers );
+ }
+
+void InsertRecordsCmd::undo()
+ {
+ removeRows( m_rowNumbers );
+ selectRow( m_rowNumbers.first() );
+ }
+
+bool InsertRecordsCmd::mergeWith( const QUndoCommand* command )
+ {
+ const InsertRecordsCmd* otherInsertCmd = static_cast<const InsertRecordsCmd*>( command );
+ if( !otherInsertCmd )
+ return false;
+ if( otherInsertCmd->m_dictModel != m_dictModel )
+ return false;
+
+ // Find where to insert the other row numbers
+ int otherFirstRow = otherInsertCmd->m_rowNumbers.first();
+ int otherRowsNum = otherInsertCmd->m_rowNumbers.size();
+ int i = 0;
+ for( i = 0; i < m_rowNumbers.size(); i++ )
+ if( otherFirstRow <= m_rowNumbers[i] )
+ break;
+ int insertPos = i;
+ // Increment this row numbers by the number of other rows, after the insertion point
+ for( int i = insertPos; i < m_rowNumbers.size(); i++ )
+ m_rowNumbers[i] += otherRowsNum;
+ // Do the insertion of other rows
+ for( int i = 0; i < otherRowsNum; i++ )
+ m_rowNumbers.insert( insertPos + i, otherInsertCmd->m_rowNumbers[i] );
+
+ updateText();
+ return true;
+ }
+
+//==============================================================================================================
+
+RemoveRecordsCmd::RemoveRecordsCmd( const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ int recordsNum = 0;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ foreach( QModelIndex index, selectedRows )
+ {
+ int row = index.row();
+ DicRecord record = index.data( DictTableModel::DicRecordRole ).value<DicRecord>();
+ Q_ASSERT( !index.data( DictTableModel::DicRecordRole ).isNull() );
+ m_records.insert( row, new DicRecord( record ) );
+ recordsNum++;
+ }
+ setText(QObject::tr( "Remove %n record(s)", "Undo action of removing records", recordsNum ));
+ }
+
+RemoveRecordsCmd::~RemoveRecordsCmd()
+ {
+ qDeleteAll( m_records );
+ }
+
+void RemoveRecordsCmd::redo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
+
+void RemoveRecordsCmd::undo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+//==============================================================================================================
+
+EditRecordCmd::EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue ):
+ m_dictModel( aTableModel ), m_insertedRow( -1 )
+ {
+ m_row = aIndex.row();
+ m_col = aIndex.column();
+ m_oldStr = aIndex.data( Qt::EditRole ).toString();
+ m_newStr = aValue.toString();
+ if( m_row == m_dictModel->rowCount() - 1 && m_col == m_dictModel->columnCount() - 1 )
+ m_insertedRow = m_dictModel->rowCount();
+ QString editStr = m_newStr;
+ if( editStr.length() > MaxMenuEditStrLen )
+ editStr = editStr.left( MaxMenuEditStrLen ) + QChar( 8230 );
+ setText( QObject::tr( "Edit \"%1\"", "Undo action of editing a record").arg( editStr ) );
+ }
+
+void EditRecordCmd::redo()
+ {
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_newStr, Qt::EditRole );
+ if( m_insertedRow > -1 )
+ m_dictModel->insertRows( m_insertedRow, 1 );
+ }
+
+void EditRecordCmd::undo()
+ {
+ if( m_insertedRow > -1 )
+ m_dictModel->removeRows( m_insertedRow, 1 );
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_oldStr, Qt::EditRole );
+ }
+
+//==============================================================================================================
+
+PasteRecordsCmd::PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // Find position where to insert records
+ // Paste above selection. If no selection -> paste in the end of dictionary
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ int pastePos = m_dictModel->rowCount();
+ if( !selectedRows.isEmpty() )
+ {
+ qSort( selectedRows );
+ QModelIndex posIndex = selectedRows.first();
+
+ // If proxy view, convert position to source index
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ posIndex = proxyModel->mapToSource( posIndex );
+
+ pastePos = posIndex.row();
+ }
+ for( int i = 0; i < aRecords.size(); i++ )
+ m_records.insert( pastePos + i, aRecords.value( i ) );
+
+ setText(QObject::tr("Paste %n record(s)", "Undo action of pasting records", m_records.size() ));
+ }
+
+PasteRecordsCmd::~PasteRecordsCmd()
+ {
+ qDeleteAll( m_records.values() );
+ }
+
+void PasteRecordsCmd::redo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+void PasteRecordsCmd::undo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
diff --git a/src/main-view/UndoCommands.h b/src/main-view/UndoCommands.h
new file mode 100644
index 0000000..a4affec
--- /dev/null
+++ b/src/main-view/UndoCommands.h
@@ -0,0 +1,109 @@
+#ifndef UNDOCOMMANDS_H
+#define UNDOCOMMANDS_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class DicRecord;
+class MainWindow;
+class DictTableView;
+class DictTableModel;
+class CardFilterModel;
+
+class UndoRecordCmd: public QUndoCommand
+{
+public:
+ UndoRecordCmd( const MainWindow* aMainWin );
+
+protected:
+ const DictTableView* getCurView();
+ CardFilterModel* getProxyModel( const DictTableView* aCurDictView );
+
+ void insertRows( QList<int> aRowNumbers );
+ void setRecords( QMap<int, DicRecord*> aRecords );
+ void removeRows( QList<int> aRowNumbers );
+ void selectRows( QList<int> aRowNumbers );
+ void selectRow( int aRow );
+
+protected:
+ const MainWindow* m_mainWindow;
+ DictTableModel* m_dictModel;
+};
+
+class InsertRecordsCmd: public UndoRecordCmd
+{
+public:
+ InsertRecordsCmd(const MainWindow* aMainWin);
+
+ static const int Id = 1;
+
+ void updateText();
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* command );
+ int id() const { return Id; }
+
+private:
+ QList<int> m_rowNumbers;
+};
+
+class RemoveRecordsCmd: public UndoRecordCmd
+{
+public:
+ RemoveRecordsCmd( const MainWindow* aMainWin );
+ ~RemoveRecordsCmd();
+
+ static const int Id = 2;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+ // The first element is the top row
+};
+
+/// Doesn't inherit UndoRecordCmd!
+class EditRecordCmd: public QUndoCommand
+{
+public:
+ EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue );
+
+ static const int Id = 3;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ static const int MaxMenuEditStrLen = 20;
+
+ QAbstractItemModel* m_dictModel;
+ int m_row;
+ int m_col;
+ QString m_oldStr;
+ QString m_newStr;
+ int m_insertedRow;
+};
+
+class PasteRecordsCmd: public UndoRecordCmd
+{
+public:
+ PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin );
+ ~PasteRecordsCmd();
+
+ static const int Id = 4;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+};
+
+#endif // UNDOCOMMANDS_H
diff --git a/src/main-view/WelcomeScreen.cpp b/src/main-view/WelcomeScreen.cpp
new file mode 100644
index 0000000..806a57a
--- /dev/null
+++ b/src/main-view/WelcomeScreen.cpp
@@ -0,0 +1,51 @@
+#include "WelcomeScreen.h"
+
+WelcomeScreen::WelcomeScreen(QWidget* parent):
+ QWidget(parent)
+{
+ QPushButton* newButton = createWelcomeButton("filenew",
+ tr("Create new dictionary"), SLOT(newFile()));
+ QPushButton* openButton = createWelcomeButton("fileopen",
+ tr("Open existing dictionary"), SLOT(openFileWithDialog()));
+ QPushButton* openExampleButton = createWelcomeButton("",
+ tr("Open online dictionaries"), SLOT(openOnlineDictionaries()));
+ QPushButton* importButton = createWelcomeButton("",
+ tr("Import from CSV file"), SLOT(importFromCsv()));
+ recentFilesButton = createWelcomeButton("",
+ tr("Recent dictionaries"), NULL);
+
+ QVBoxLayout* buttonsLt = new QVBoxLayout;
+ buttonsLt->addWidget(newButton);
+ buttonsLt->addWidget(openButton);
+ buttonsLt->addWidget(openExampleButton);
+ buttonsLt->addWidget(importButton);
+ buttonsLt->addWidget(recentFilesButton);
+
+ QGridLayout* paddingLt = new QGridLayout;
+ paddingLt->addLayout(buttonsLt, 1, 1);
+ setLayout(paddingLt);
+}
+
+QPushButton* WelcomeScreen::createWelcomeButton(const QString& iconName,
+ const QString& text, const char* slot)
+{
+ static const QSize ButtonSize(350, 50);
+ QPushButton* button = new QPushButton(QIcon(QString(":/images/%1.png").
+ arg(iconName)), text);
+ button->setMaximumSize(ButtonSize);
+ if(slot)
+ connect(button, SIGNAL(clicked()), parentWidget(), slot);
+ return button;
+}
+
+void WelcomeScreen::updateRecentFilesButton()
+{
+ recentFilesButton->setEnabled(recentFilesButton->menu() &&
+ !recentFilesButton->menu()->actions().isEmpty());
+}
+
+void WelcomeScreen::setRecentFilesMenu(QMenu* menu)
+{
+ recentFilesButton->setMenu(menu);
+ updateRecentFilesButton();
+}
diff --git a/src/main-view/WelcomeScreen.h b/src/main-view/WelcomeScreen.h
new file mode 100644
index 0000000..17ca18e
--- /dev/null
+++ b/src/main-view/WelcomeScreen.h
@@ -0,0 +1,25 @@
+#ifndef WELCOME_SCREEN_H
+#define WELCOME_SCREEN_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class WelcomeScreen: public QWidget
+{
+ Q_OBJECT
+
+public:
+ WelcomeScreen(QWidget* parent);
+ void setRecentFilesMenu(QMenu* menu);
+
+public slots:
+ void updateRecentFilesButton();
+
+private:
+ QPushButton* createWelcomeButton(const QString& iconName, const QString& text,
+ const char* slot);
+
+private:
+ QPushButton* recentFilesButton;
+};
+#endif