diff options
Diffstat (limited to 'src/main-view')
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 © ..."), 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 |