summaryrefslogtreecommitdiff
path: root/src/main-view/MainWindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/main-view/MainWindow.cpp')
-rw-r--r--src/main-view/MainWindow.cpp1313
1 files changed, 1313 insertions, 0 deletions
diff --git a/src/main-view/MainWindow.cpp b/src/main-view/MainWindow.cpp
new file mode 100644
index 0000000..e5b285c
--- /dev/null
+++ b/src/main-view/MainWindow.cpp
@@ -0,0 +1,1313 @@
+#include "MainWindow.h"
+#include "AppModel.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "FindPanel.h"
+#include "DictTableDelegate.h"
+#include "PacksTreeModel.h"
+#include "AboutDialog.h"
+#include "RecentFilesManager.h"
+#include "LanguageMenu.h"
+#include "CardPreview.h"
+#include "WelcomeScreen.h"
+#include "../version.h"
+#include "../strings.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/DicCsvWriter.h"
+#include "../dictionary/DicCsvReader.h"
+#include "../dic-options/DictionaryOptionsDialog.h"
+#include "../export-import/CsvExportDialog.h"
+#include "../export-import/CsvImportDialog.h"
+#include "../settings/FontColorSettingsDialog.h"
+#include "../settings/StudySettingsDialog.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../study/WordDrillWindow.h"
+#include "../study/SpacedRepetitionWindow.h"
+#include "../study/CardSideView.h"
+#include "../utils/RandomGenerator.h"
+#include "../statistics/StatisticsView.h"
+
+MainWindow::MainWindow(AppModel* model):
+ workPath(""),
+ addImagePath(""),
+ model(model), welcomeScreen(NULL), studyWindow(NULL)
+{
+ init();
+ createCentralWidget();
+ createActions();
+ createMenus();
+ createToolBars();
+ createStatusBar();
+ FieldStyleFactory::inst()->load();
+ createDockWindows();
+ readSettings();
+ openSession();
+}
+
+MainWindow::~MainWindow()
+{
+ delete studyWindow;
+}
+
+void MainWindow::init()
+{
+ setWindowTitle(Strings::tr(Strings::s_appTitle));
+ setWindowIcon(QIcon(":/images/freshmemory.png"));
+}
+
+void MainWindow::activate()
+{
+ activateWindow();
+ raise();
+}
+
+void MainWindow::updateDictTab()
+{
+ int curIndex = dictTabWidget->currentIndex();
+ const Dictionary* curDict = model->curDictionary();
+ if(!curDict)
+ return;
+ if(!curDict->contentModified())
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/openbook-24.png") );
+ else
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/filesave.png") );
+ dictTabWidget->setTabText(curIndex, curDict->shortName());
+ dictTabWidget->setTabToolTip(curIndex, QDir::toNativeSeparators( curDict->getFilePath()));
+}
+
+void MainWindow::updateTotalRecordsLabel()
+{
+ const Dictionary* curDict = model->curDictionary();
+ if(curDict)
+ {
+ totalRecordsLabel->show();
+ totalRecordsLabel->setText( tr("Records: %1").arg(curDict->entriesNum()) );
+ }
+ else
+ totalRecordsLabel->hide();
+}
+
+void MainWindow::updateActions()
+{
+ const Dictionary* curDict = model->curDictionary();
+ bool isCurDictModified = curDict && curDict->contentModified();
+
+ saveAct->setEnabled( isCurDictModified );
+ saveAsAct->setEnabled( curDict );
+ saveCopyAct->setEnabled( curDict );
+ exportAct->setEnabled( curDict );
+ removeTabAct->setEnabled( curDict );
+ updatePasteAction();
+ insertRecordsAct->setEnabled( curDict );
+ findAct->setEnabled( curDict );
+ if( findPanel )
+ findAgainAct->setEnabled( findPanel->canFindAgain() );
+ wordDrillAct->setEnabled(curDict);
+ spacedRepetitionAct->setEnabled(curDict);
+ statisticsAct->setEnabled(curDict);
+ dictionaryOptionsAct->setEnabled(curDict);
+}
+
+void MainWindow::updateAddImageAction()
+{
+ addImageAct->setEnabled(dictTabWidget->isInEditingState());
+}
+
+void MainWindow::updateSelectionActions()
+ {
+ if( !model->curDictionary() )
+ return;
+ const DictTableView* tableView = getCurDictView();
+ if( !tableView )
+ return;
+ Q_ASSERT( tableView->selectionModel() );
+ bool hasSelection = tableView->selectionModel()->hasSelection();
+ foreach( QAction* action, selectionActions )
+ action->setEnabled( hasSelection );
+ }
+
+void MainWindow::updatePasteAction()
+{
+ bool isCurTabValid = dictTabWidget->currentIndex() >= 0;
+ bool hasClipboardText = !QApplication::clipboard()->text().isEmpty();
+ pasteAct->setEnabled( isCurTabValid && hasClipboardText );
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+if( proposeToSave() )
+ {
+ writeSettings();
+ event->accept();
+ qApp->quit();
+ }
+else
+ event->ignore();
+}
+
+void MainWindow::newFile()
+ {
+ addDictTab(model->newDictionary());
+ updatePacksTreeView();
+ }
+
+void MainWindow::openFileWithDialog()
+ {
+ openFileWithDialog(workPath);
+ }
+
+void MainWindow::openOnlineDictionaries()
+ {
+ QDesktopServices::openUrl(QUrl("http://fresh-memory.com/dictionaries"));
+ }
+
+void MainWindow::openFileWithDialog(const QString& dirPath)
+ {
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Open dictionary"), dirPath,
+ tr("Dictionaries", "Filter name in dialog")+ " (*.fmd)");
+ if( filePath.isEmpty() )
+ return;
+ openFile(filePath);
+ }
+
+void MainWindow::openFile(const QString& filePath)
+ {
+ QString internalFilePath = QDir::fromNativeSeparators(filePath);
+ workPath = QFileInfo(internalFilePath).path();
+ int dictIndex = model->indexOfDictionary(internalFilePath);
+ if(isDictOpened(dictIndex))
+ setCurDictionary(dictIndex);
+ else
+ if(!loadFile(internalFilePath))
+ return;
+ updateAfterOpenedDictionary(internalFilePath);
+ }
+
+bool MainWindow::isDictOpened(int index)
+{
+ return index > -1;
+}
+
+void MainWindow::setCurDictionary(int index)
+{
+ if(index == -1)
+ return;
+ model->setCurDictionary(index);
+ setCurDictTab(index);
+}
+
+bool MainWindow::loadFile(const QString& filePath)
+{
+ bool ok = model->openDictionary(filePath);
+ if(ok)
+ addDictTab(model->curDictionary());
+ else
+ showOpenFileError();
+ return ok;
+}
+
+void MainWindow::showOpenFileError()
+{
+ QMessageBox::critical(this, Strings::errorTitle(), model->getErrorMessage());
+}
+
+void MainWindow::updateAfterOpenedDictionary(const QString& filePath)
+{
+ recentFilesMan->addFile(filePath);
+ updatePacksTreeView();
+ updateCardPreview();
+}
+
+void MainWindow::addDictTab(Dictionary* dict)
+ {
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateActions()) );
+ connect( dict, SIGNAL(studyModifiedChanged(bool)), SLOT(saveStudyWithDelay(bool)) );
+ connect( dict, SIGNAL(filePathChanged()), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(entriesInserted(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(entriesRemoved(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updatePacksTreeView()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updateCardPreview()) );
+ connect( dict, SIGNAL(destroyed()), SLOT(updatePacksTreeView()) );
+
+ dictTabWidget->addDictTab(model->curDictModel());
+ DictTableView* tableView = const_cast<DictTableView*>( dictTabWidget->curDictView() );
+
+ tableView->addActions(contextMenuActions);
+ tableView->verticalHeader()->addActions(contextMenuActions);
+ tableView->setContextMenuPolicy( Qt::ActionsContextMenu );
+ tableView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu );
+
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateSelectionActions()) );
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateCardPreview()) );
+ connect( tableView, SIGNAL(destroyed(QAbstractItemModel*)), model, SLOT(removeDictModel(QAbstractItemModel*)) );
+ updateDictTab();
+ updateTotalRecordsLabel();
+ updateSelectionActions();
+ packsTreeView->reset();
+ QModelIndex firstIx = tableView->model()->index(0, 0, QModelIndex());
+ tableView->setCurrentIndex(firstIx);
+ tableView->setFocus();
+ }
+
+const DictTableView* MainWindow::getCurDictView() const
+ {
+ if(studyWindow)
+ {
+ const DictTableView* editCardView = studyWindow->cardEditView();
+ if( editCardView )
+ return editCardView;
+ }
+ return dictTabWidget->curDictView();
+ }
+
+/// Fork to SaveAs() or really save in DoSave()
+bool MainWindow::Save()
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() || dict->nameIsTemp() )
+ return SaveAs();
+else
+ return doSave( filePath );
+}
+
+bool MainWindow::SaveAs( bool aChangeFilePath )
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() )
+ filePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+filePath = QFileDialog::getSaveFileName(this, tr("Save dictionary as ..."), filePath);
+if( filePath.isEmpty() )
+ return false;
+workPath = QFileInfo( filePath ).path();
+if( doSave( filePath, aChangeFilePath ) )
+ {
+ recentFilesMan->addFile(filePath);
+ return true;
+ }
+else
+ return false;
+}
+
+void MainWindow::SaveCopy()
+{
+ SaveAs( false ); // Do not change the file name
+}
+
+void MainWindow::importFromCsv()
+{
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Import CSV file"), workPath);
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ CsvImportDialog importDialog( this, filePath, model );
+ if( importDialog.exec() )
+ {
+ model->addDictionary( importDialog.getDictionary() );
+ addDictTab(importDialog.getDictionary());
+ importDialog.getDictionary()->generateCards();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::exportToCsv()
+{
+ Dictionary* dict = model->curDictionary();
+ if( !dict )
+ return;
+ CsvExportDialog exportDialog( this, dict );
+ if( exportDialog.exec() )
+ {
+ QString dictFilePath( dict->getFilePath() );
+ if( dictFilePath.isEmpty() )
+ dictFilePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+ QString filePath = QFileDialog::getSaveFileName( this, tr("Export to CSV file"), dictFilePath + ".txt" );
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ exportDialog.SaveCSVToFile( filePath );
+ }
+}
+
+bool MainWindow::doSave( const QString& aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = model->curDictionary()->save( aFilePath, aChangeFilePath );
+ if( error != QFile::NoError )
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), (tr("Cannot save dictionary:") + "\n%1").
+ arg(QDir::toNativeSeparators(aFilePath)) );
+ return false;
+ }
+ dictTabWidget->cleanUndoStack();
+ updateDictTab();
+ updateActions();
+ return true;
+}
+
+bool MainWindow::saveStudy()
+{
+ QTime saveTime;
+ saveTime.start();
+ Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return false;
+ if(dict->saveStudy() != QFile::NoError)
+ {
+ QMessageBox::warning(this, Strings::errorTitle(),
+ (tr("Cannot save study file:") + "\n%1").
+ arg(QDir::toNativeSeparators(
+ model->curDictionary()->getStudyFilePath())));
+ return false;
+ }
+ return true;
+}
+
+void MainWindow::saveStudyWithDelay(bool studyModified)
+{
+ if(studyModified)
+ QTimer::singleShot(AutoSaveStudyInterval, this, SLOT(saveStudy()));
+}
+
+/**
+ * Checks if all files are saved. If not, proposes to the user to save them.
+ * This function is called before closing the application.
+ * @return true, if all files are now saved and the application can be closed
+ */
+bool MainWindow::proposeToSave()
+{
+for(int i=0; i<dictTabWidget->count(); i++)
+ if( !proposeToSave(i) )
+ return false;
+return true;
+}
+
+/**
+ * Checks if the specified file is modified and if it is, proposes to the user to save it.
+ * If the specified file is not modified, the function just returns true.
+ *
+ * Function silently saves the study data, if it is modified.
+ *
+ * @param aTabIndex Index of the tab containing the modified file
+ * @return true, if the file is saved and the application can be closed.
+ * false, if the user declined the proposition to save the file.
+ */
+bool MainWindow::proposeToSave(int aTabIndex)
+{
+const Dictionary* dict = model->dictionary( aTabIndex );
+
+if( dict->contentModified() )
+ {
+ dictTabWidget->setCurrentIndex( aTabIndex );
+ QMessageBox::StandardButton pressedButton;
+ pressedButton = QMessageBox::warning( this, tr("Save dictionary?"), tr("Dictionary %1 was modified.\n"
+ "Save changes?").arg( dict->shortName(false) ),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel );
+ if( pressedButton == QMessageBox::Yes )
+ {
+ bool saved = Save();
+ if( !saved )
+ return false;
+ }
+ else if( pressedButton == QMessageBox::Cancel )
+ return false;
+ }
+if(dict->studyModified())
+ saveStudy();
+return true;
+}
+
+void MainWindow::openSession()
+{
+ QSettings settings;
+ QStringList sessionFiles = settings.value("session").toStringList();
+ if(sessionFiles.isEmpty())
+ {
+ QString lastFile = recentFilesMan->getLastUsedFilePath();
+ if(lastFile.isEmpty())
+ return;
+ sessionFiles << recentFilesMan->getLastUsedFilePath();
+ }
+ foreach(QString fileName, sessionFiles)
+ openFile(fileName);
+ int curTab = settings.value("session-cur-tab").toInt();
+ setCurDictionary(curTab);
+}
+
+void MainWindow::help()
+ {
+ QString fullVersion = QString(FM_VERSION);
+ QString version = fullVersion.left(fullVersion.lastIndexOf('.'));
+ QString url = QString("http://fresh-memory.com/docs/%1/index.html").arg(version);
+ QDesktopServices::openUrl(QUrl(url));
+ }
+
+void MainWindow::about()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+ dialog.setParent(NULL);
+}
+
+void MainWindow::addImage()
+ {
+ int cursorPos = getCurEditorCursorPos();
+ if(cursorPos < 0)
+ return;
+ const_cast<DictTableView*>(getCurDictView())->commitEditing();
+ QString selectedFile = selectAddImageFile();
+ if(selectedFile.isEmpty())
+ return;
+ selectedFile = copyImageFileToImagesDir(selectedFile);
+ if(selectedFile.isEmpty())
+ return;
+ insertImageIntoCurEditor(cursorPos, selectedFile);
+ }
+
+QString MainWindow::selectAddImageFile()
+ {
+ checkAddImagePath();
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Add image"),
+ addImagePath,
+ tr("Images", "Filter name in dialog") +
+ " (*.png *.jpg *.jpeg *.gif *.svg *.xpm *.ico *.mng *.tiff);; " +
+ tr("All files") + " (*.*)");
+ if(filePath.isEmpty())
+ return QString();
+ addImagePath = QFileInfo(filePath).path();
+ return filePath;
+ }
+
+void MainWindow::checkAddImagePath()
+{
+ if(!addImagePath.isEmpty())
+ return;
+ if(!getCurDict())
+ return;
+ QString dicFilePath = getCurDict()->getFilePath();
+ addImagePath = QFileInfo(dicFilePath).path();
+}
+
+QString MainWindow::copyImageFileToImagesDir(const QString& filePath)
+{
+ QString dicImagesDir = getCurDict()->getImagesPath();
+ QString imageDir = QFileInfo(filePath).path();
+ if(imageDir == dicImagesDir)
+ return filePath;
+ QString newFilePath = createNewImageFilePath(dicImagesDir, filePath);
+ if(!QFileInfo(dicImagesDir).exists())
+ QDir(dicImagesDir).mkpath(".");
+ if(!QFile::copy(filePath, newFilePath))
+ return QString();
+ return newFilePath;
+}
+
+QString MainWindow::createNewImageFilePath(const QString& dicImagesDir,
+ const QString& filePath)
+{
+ int num = 0;
+ QString newFilePath;
+ do
+ {
+ newFilePath = createImagesDirFilePath(dicImagesDir, filePath, num);
+ num++;
+ }
+ while(QFileInfo(newFilePath).exists());
+ return newFilePath;
+}
+
+QString MainWindow::createImagesDirFilePath(const QString& dicImagesDir,
+ const QString& filePath, int suffixNum)
+{
+ QString imageFileName = QFileInfo(filePath).completeBaseName();
+ if(suffixNum > 0)
+ imageFileName += "-" + QString::number(suffixNum);
+ imageFileName += "." + QFileInfo(filePath).suffix();
+ return QDir(dicImagesDir).filePath(imageFileName);
+}
+
+int MainWindow::getCurEditorCursorPos()
+ {
+ const DictTableView* dictView = getCurDictView();
+ if(!dictView)
+ return -1;
+ return dictView->getEditorCursorPos();
+ }
+
+int MainWindow::getCurRow()
+{
+ return getCurDictView()->currentIndex().row();
+}
+
+int MainWindow::getCurColumn()
+{
+ return getCurDictView()->currentIndex().column();
+}
+
+Dictionary* MainWindow::getCurDict()
+{
+ return model->curDictionary();
+}
+
+void MainWindow::insertImageIntoCurEditor(int cursorPos, const QString& filePath)
+{
+ const DictTableView* dictView = getCurDictView();
+ const_cast<DictTableView*>(dictView)->startEditing(getCurRow(), getCurColumn());
+ dictView->insertImageIntoEditor(cursorPos, filePath);
+}
+
+void MainWindow::insertRecords()
+ {
+ pushToUnoStack(new InsertRecordsCmd(this));
+ }
+
+void MainWindow::removeRecords()
+ {
+ pushToUnoStack(new RemoveRecordsCmd(this));
+ }
+
+void MainWindow::pushToUnoStack(QUndoCommand* command)
+ {
+ getCurDictView()->dicTableModel()->undoStack()->push(command);
+ }
+
+void MainWindow::find()
+ {
+ findPanel->show();
+ findAgainAct->setEnabled(true);
+ }
+
+void MainWindow::findAgain()
+ {
+ findPanel->find();
+ }
+
+void MainWindow::createActions()
+{
+ createFileActions();
+ createEditActions();
+ createDictContextMenuActions();
+ createSelectionActions();
+ createToolsActions();
+ createSettingsActions();
+ createHelpActions();
+ initActions();
+}
+
+void MainWindow::createFileActions()
+{
+ newAct = new QAction(QIcon(":/images/filenew.png"), tr("&New"), this);
+ newAct->setShortcut(tr("Ctrl+N"));
+ connect(newAct, SIGNAL(triggered()), SLOT(newFile()));
+
+ loadAct = new QAction(QIcon(":/images/fileopen.png"), tr("&Open ..."), this);
+ loadAct->setShortcut(tr("Ctrl+O"));
+ connect(loadAct, SIGNAL(triggered()), SLOT(openFileWithDialog()));
+
+ openFlashcardsAct = new QAction(tr("Online dictionaries"), this);
+ connect(openFlashcardsAct, SIGNAL(triggered()), SLOT(openOnlineDictionaries()));
+
+ saveAct = new QAction(QIcon(":/images/filesave.png"), tr("&Save"), this);
+ saveAct->setShortcut(tr("Ctrl+S"));
+ connect(saveAct, SIGNAL(triggered()), SLOT(Save()));
+
+ saveAsAct = new QAction(QIcon(":/images/filesaveas.png"), tr("Save &as ..."), this);
+ connect(saveAsAct, SIGNAL(triggered()), SLOT(SaveAs()));
+
+ saveCopyAct = new QAction(tr("Save &copy ..."), this);
+ connect(saveCopyAct, SIGNAL(triggered()), SLOT(SaveCopy()));
+
+ importAct = new QAction(tr("&Import from CSV ..."), this);
+ connect(importAct, SIGNAL(triggered()), SLOT(importFromCsv()));
+
+ exportAct = new QAction(tr("&Export to CSV ..."), this);
+ connect(exportAct, SIGNAL(triggered()), SLOT(exportToCsv()));
+
+ removeTabAct = new QAction(QIcon(":/images/remove.png"),
+ tr("&Close dictionary"), this);
+ removeTabAct->setShortcut(tr("Ctrl+W"));
+ connect(removeTabAct, SIGNAL(triggered()), dictTabWidget, SLOT(closeTab()));
+
+ quitAct = new QAction(QIcon(":/images/exit.png"), tr("&Quit"), this);
+ quitAct->setShortcut(tr("Ctrl+Q"));
+ connect(quitAct, SIGNAL(triggered()), SLOT(close()));
+}
+
+void MainWindow::createEditActions()
+{
+ undoAct = dictTabWidget->undoGroup()->createUndoAction(this);
+ undoAct->setShortcut(tr("Ctrl+Z"));
+
+ redoAct = dictTabWidget->undoGroup()->createRedoAction(this);
+ redoAct->setShortcut(tr("Ctrl+Y"));
+
+ copyAct = new QAction(QIcon(":/images/editcopy.png"), tr("&Copy"), this);
+ copyAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+C") << QString("Ctrl+Insert"));
+ connect(copyAct, SIGNAL(triggered()), SLOT(copyEntries()));
+
+ cutAct = new QAction(QIcon(":/images/editcut.png"), tr("Cu&t"), this);
+ cutAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+X") << QString("Shift+Delete"));
+ connect(cutAct, SIGNAL(triggered()), SLOT(cutEntries()));
+
+ pasteAct = new QAction(QIcon(":/images/editpaste.png"), tr("&Paste"), this);
+ pasteAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+V") << QString("Shift+Insert"));
+ connect(pasteAct, SIGNAL(triggered()), SLOT(pasteEntries()));
+ connect(QApplication::clipboard(), SIGNAL(dataChanged()), SLOT(updatePasteAction()));
+
+ createAddImageAct();
+ createinsertRecordsAct();
+ createRemoveRecordsAct();
+
+ findAct = new QAction(QIcon(":/images/find.png"), tr("&Find..."), this);
+ findAct->setShortcut(tr("Ctrl+F"));
+ connect(findAct, SIGNAL(triggered()), SLOT(find()));
+
+ findAgainAct = new QAction(QIcon(":/images/next.png"), tr("Find &again"), this);
+ findAgainAct->setShortcut(QString("F3"));
+ connect(findAgainAct, SIGNAL(triggered()), SLOT(findAgain()));
+}
+
+void MainWindow::createAddImageAct()
+{
+ addImageAct = new QAction(QIcon(":/images/add-image.png"), tr("&Add image"), this);
+ addImageAct->setShortcut(tr("Ctrl+G"));
+ connect(addImageAct, SIGNAL(triggered()), SLOT(addImage()));
+ connect(dictTabWidget, SIGNAL(editingStateChanged()), SLOT(updateAddImageAction()));
+}
+
+void MainWindow::createinsertRecordsAct()
+{
+ insertRecordsAct = new QAction(QIcon(":/images/add.png"),
+ tr("&Insert record"), this);
+ insertRecordsAct->setShortcut(tr("Ctrl+I"));
+ connect(insertRecordsAct, SIGNAL(triggered()), SLOT(insertRecords()));
+}
+
+void MainWindow::createRemoveRecordsAct()
+{
+ removeRecordsAct = new QAction(QIcon(":/images/delete.png"),
+ tr("&Remove record"), this);
+ removeRecordsAct->setShortcut(Qt::Key_Delete);
+ connect(removeRecordsAct, SIGNAL(triggered()), SLOT(removeRecords()));
+}
+
+void MainWindow::createToolsActions()
+{
+ QSignalMapper* startStudySignalMapper = new QSignalMapper(this);
+ connect(startStudySignalMapper, SIGNAL(mapped(int)), SLOT(startStudy(int)));
+
+ wordDrillAct = new QAction(QIcon(":/images/word-drill.png"), tr("&Word drill"),
+ this);
+ wordDrillAct->setShortcut(QString("F5"));
+ connect(wordDrillAct, SIGNAL(triggered()), startStudySignalMapper, SLOT(map()));
+ startStudySignalMapper->setMapping(wordDrillAct, AppModel::WordDrill);
+
+ spacedRepetitionAct = new QAction(QIcon(":/images/spaced-rep.png"),
+ tr("&Spaced repetition"), this);
+ spacedRepetitionAct->setShortcut(QString("F6"));
+ connect(spacedRepetitionAct, SIGNAL(triggered()), startStudySignalMapper,
+ SLOT(map()));
+ startStudySignalMapper->setMapping(spacedRepetitionAct, AppModel::SpacedRepetition);
+
+ statisticsAct = new QAction(QIcon(":/images/statistics.png"), tr("S&tatistics"),
+ this);
+ statisticsAct->setShortcut(QString("F7"));
+ connect(statisticsAct, SIGNAL(triggered()), SLOT(showStatistics()));
+}
+
+void MainWindow::createSettingsActions()
+{
+ dictionaryOptionsAct = new QAction(QIcon(":/images/dic-options.png"),
+ tr("&Dictionary options"), this);
+ dictionaryOptionsAct->setShortcut(QString("Ctrl+1"));
+ connect(dictionaryOptionsAct, SIGNAL(triggered()), SLOT(openDictionaryOptions()));
+
+ fontColorSettingsAct = new QAction(QIcon(":/images/font-style.png"),
+ tr("&Font and color settings"), this);
+ fontColorSettingsAct->setShortcut(QString("Ctrl+2"));
+ connect(fontColorSettingsAct, SIGNAL(triggered()), SLOT(openFontColorSettings()));
+
+ studySettingsAct = new QAction(QIcon(":/images/study-settings.png"),
+ tr("&Study settings"), this);
+ studySettingsAct->setShortcut(QString("Ctrl+3"));
+ connect(studySettingsAct, SIGNAL(triggered()), SLOT(openStudySettings()));
+}
+
+void MainWindow::createHelpActions()
+{
+ helpAct = new QAction(tr("Help"), this);
+ helpAct->setShortcut(QString("F1"));
+ connect(helpAct, SIGNAL(triggered()), SLOT(help()));
+
+ aboutAct = new QAction(QIcon(":/images/freshmemory.png"), tr("About"), this);
+ connect(aboutAct, SIGNAL(triggered()), SLOT(about()));
+}
+
+void MainWindow::initActions()
+{
+ saveAct->setEnabled(false);
+ saveAsAct->setEnabled(false);
+ saveCopyAct->setEnabled(false);
+ exportAct->setEnabled(false);
+ removeTabAct->setEnabled(false);
+
+ cutAct->setEnabled(false);
+ copyAct->setEnabled(false);
+ pasteAct->setEnabled(false);
+ addImageAct->setEnabled(false);
+ insertRecordsAct->setEnabled(false);
+ removeRecordsAct->setEnabled(false);
+ findAct->setEnabled(false);
+ findAgainAct->setEnabled(false);
+
+ statisticsAct->setEnabled(false);
+ wordDrillAct->setEnabled(false);
+ spacedRepetitionAct->setEnabled(false);
+
+ dictionaryOptionsAct->setEnabled(false);
+}
+
+void MainWindow::createDictContextMenuActions()
+{
+ contextMenuActions << copyAct;
+ contextMenuActions << cutAct;
+ contextMenuActions << pasteAct;
+ contextMenuActions << insertRecordsAct;
+ contextMenuActions << removeRecordsAct;
+}
+
+void MainWindow::createSelectionActions()
+{
+ selectionActions << copyAct;
+ selectionActions << cutAct;
+ selectionActions << removeRecordsAct;
+}
+
+void MainWindow::createMenus()
+{
+ createFileMenu();
+
+ editMenu = menuBar()->addMenu( tr("&Edit") );
+ editMenu->addAction( undoAct );
+ editMenu->addAction( redoAct );
+ editMenu->addSeparator();
+
+ editMenu->addAction(cutAct);
+ editMenu->addAction(copyAct);
+ editMenu->addAction(pasteAct);
+ editMenu->addSeparator();
+
+ editMenu->addAction(addImageAct);
+ editMenu->addAction(insertRecordsAct);
+ editMenu->addAction(removeRecordsAct);
+ editMenu->addSeparator();
+ editMenu->addAction(findAct);
+ editMenu->addAction(findAgainAct);
+
+ viewMenu = menuBar()->addMenu( tr("&View") );
+
+ toolsMenu = menuBar()->addMenu( tr("&Tools") );
+ toolsMenu->addAction(wordDrillAct);
+ toolsMenu->addAction(spacedRepetitionAct);
+ toolsMenu->addAction(statisticsAct);
+
+ createOptionsMenu();
+
+ menuBar()->addSeparator();
+
+ helpMenu = menuBar()->addMenu( tr("&Help") );
+ helpMenu->addAction(helpAct);
+ helpMenu->addAction(aboutAct);
+}
+
+void MainWindow::createFileMenu()
+{
+ fileMenu = menuBar()->addMenu(tr("&File"));
+ fileMenu->addAction(newAct);
+ fileMenu->addAction(loadAct);
+ fileMenu->addAction(openFlashcardsAct);
+ fileMenu->addAction(saveAct);
+ fileMenu->addAction(saveAsAct);
+ fileMenu->addAction(saveCopyAct);
+ fileMenu->addAction(importAct);
+ fileMenu->addAction(exportAct);
+
+ createRecentFilesMenu();
+
+ fileMenu->addSeparator();
+ fileMenu->addAction(removeTabAct);
+ fileMenu->addAction(quitAct);
+}
+
+void MainWindow::createRecentFilesMenu()
+{
+ QMenu* recentFilesMenu = fileMenu->addMenu(tr("&Recent files"));
+ welcomeScreen->setRecentFilesMenu(recentFilesMenu);
+ recentFilesMan = new RecentFilesManager(this, recentFilesMenu);
+ connect(recentFilesMan, SIGNAL(recentFileTriggered(const QString&)),
+ SLOT(openFile(const QString&)));
+ connect(recentFilesMan, SIGNAL(addedFile()), welcomeScreen,
+ SLOT(updateRecentFilesButton()));
+}
+
+void MainWindow::createOptionsMenu()
+{
+ optionsMenu = menuBar()->addMenu(tr("&Options"));
+ optionsMenu->addAction( dictionaryOptionsAct );
+ optionsMenu->addAction( fontColorSettingsAct );
+ optionsMenu->addAction( studySettingsAct );
+
+ new LanguageMenu(optionsMenu);
+}
+
+void MainWindow::createToolBars()
+{
+ QToolBar* toolBar = addToolBar( tr("Main") );
+ toolBar->setObjectName("Main");
+ toolBar->addAction(newAct);
+ toolBar->addAction(loadAct);
+ toolBar->addAction(saveAct);
+ toolBar->addSeparator();
+ toolBar->addAction(findAct);
+ toolBar->addAction(wordDrillAct);
+ toolBar->addAction(spacedRepetitionAct);
+ toolBar->addAction(statisticsAct);
+ toolBar->addAction(addImageAct);
+}
+
+void MainWindow::createStatusBar()
+{
+ statusBar()->addPermanentWidget( totalRecordsLabel = new QLabel );
+ updateTotalRecordsLabel();
+}
+
+void MainWindow::createCentralWidget()
+ {
+ mainStackedWidget = new QStackedWidget;
+ welcomeScreen = new WelcomeScreen(this);
+ mainStackedWidget->addWidget(welcomeScreen);
+ mainStackedWidget->addWidget(createDictionaryView());
+ setCentralWidget(mainStackedWidget);
+ }
+
+QWidget* MainWindow::createDictionaryView()
+{
+ dictTabWidget = new DictionaryTabWidget(this);
+ connect(dictTabWidget, SIGNAL(currentChanged(int)), SLOT(curTabChanged(int)));
+ findPanel = new FindPanel(this);
+ findPanel->hide();
+
+ QVBoxLayout* dictionaryViewLt = new QVBoxLayout;
+ dictionaryViewLt->setContentsMargins(0, 0, 0, 0);
+ dictionaryViewLt->addWidget(dictTabWidget);
+ dictionaryViewLt->addWidget(findPanel);
+
+ QWidget* dictionaryView = new QWidget;
+ dictionaryView->setLayout(dictionaryViewLt);
+ return dictionaryView;
+}
+
+void MainWindow::curTabChanged(int tabIndex)
+{
+ updateMainStackedWidget(tabIndex);
+ model->setCurDictionary(tabIndex);
+ updateActions();
+ updateSelectionActions();
+ updateTotalRecordsLabel();
+ updatePackSelection(tabIndex);
+ updateCardPreview();
+}
+
+void MainWindow::updateMainStackedWidget(int tabIndex)
+{
+ if (tabIndex == -1)
+ mainStackedWidget->setCurrentIndex(0);
+ else
+ mainStackedWidget->setCurrentIndex(1);
+}
+
+void MainWindow::createDockWindows()
+ {
+ createPacksTreeDock();
+ createCardPreviewDock();
+ }
+
+void MainWindow::createPacksTreeDock()
+{
+ QDockWidget* packsDock = new QDockWidget(tr("Card packs"));
+ packsDock->setObjectName("Card-packs");
+ packsDock->setAllowedAreas(
+ Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+ packsTreeView = new QTreeView;
+ packsTreeView->setAllColumnsShowFocus(true);
+ packsTreeView->setRootIsDecorated(false);
+ packsTreeView->header()->setDefaultSectionSize(150);
+ PacksTreeModel* packsTreeModel = new PacksTreeModel(model);
+ packsTreeView->setModel(packsTreeModel);
+ packsDock->setWidget(packsTreeView);
+ addDockWidget(Qt::LeftDockWidgetArea, packsDock);
+ viewMenu->addAction(packsDock->toggleViewAction());
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(setCurDictTabByTreeIndex(const QModelIndex&)));
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateCardPreview()));
+ connect(packsTreeView, SIGNAL(activated(const QModelIndex&)),
+ SLOT(startSpacedRepetitionStudy()));
+}
+
+void MainWindow::createCardPreviewDock()
+{
+ cardPreview = new CardPreview(this);
+ addDockWidget(Qt::LeftDockWidgetArea, cardPreview);
+ viewMenu->addAction(cardPreview->toggleViewAction());
+}
+
+void MainWindow::updatePacksTreeView()
+{
+ static_cast<PacksTreeModel*>( packsTreeView->model() )->updateData();
+ packsTreeView->expandAll();
+ packsTreeView->resizeColumnToContents( 1 );
+ packsTreeView->resizeColumnToContents( 2 );
+ updatePackSelection();
+}
+
+void MainWindow::updateCardPreview()
+{
+ CardPack* pack = getCurCardPack();
+ Card* card = getCurCardFromPack(pack);
+ cardPreview->setContent(pack, card);
+}
+
+CardPack* MainWindow::getCurCardPack() const
+{
+ const Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return NULL;
+ int packId = packsTreeView->currentIndex().row();
+ return dict->cardPack(packId);
+}
+
+const DicRecord* MainWindow::getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const
+{
+ if(!dict || !dictView)
+ return NULL;
+ return dict->getRecord(dictView->currentIndex().row());
+}
+
+const DicRecord* MainWindow::getCurDictRecord() const
+{
+ return getCurDictRecord(model->curDictionary(), getCurDictView());
+}
+
+Card* MainWindow::getCurCardFromPack(CardPack* pack) const
+{
+ const DicRecord* record = getCurDictRecord();
+ if(!record)
+ return NULL;
+ return pack->getCard(record->getPreviewQuestionForPack(pack));
+}
+
+/// Proxy-view-aware copying
+void MainWindow::copyEntries()
+ {
+ const DictTableView* tableView = getCurDictView(); // May be proxy view
+ if( !tableView )
+ return;
+ QModelIndexList selectedIndexes = tableView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( tableView->model() );
+ if( proxyModel )
+ {
+ QModelIndexList srcIndexes;
+ foreach( QModelIndex index, selectedIndexes )
+ {
+ QModelIndex srcIndex = proxyModel->mapToSource( index );
+ srcIndexes << srcIndex;
+ }
+ selectedIndexes = srcIndexes;
+ }
+ Dictionary* dict = model->curDictionary();
+ QList<DicRecord*> entries;
+ foreach( QModelIndex index, selectedIndexes )
+ entries << const_cast<DicRecord*>(dict->getRecord( index.row() ));
+
+ CsvExportData params;
+ params.commentChar = '#';
+ params.fieldSeparators = "& ";
+ params.quoteAllFields = false;
+ params.textDelimiter = '"';
+ params.writeColumnNames = true;
+ DicCsvWriter csvWriter( entries );
+ QString copiedStr = csvWriter.toCsvString( params );
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText( copiedStr );
+ }
+
+void MainWindow::cutEntries()
+ {
+ copyEntries();
+ removeRecords();
+ }
+
+void MainWindow::pasteEntries()
+ {
+ // Get text from clipboard
+ QClipboard *clipboard = QApplication::clipboard();
+ QString pastedStr = clipboard->text();
+ if( pastedStr.isEmpty() )
+ return;
+
+ const DictTableView* dictView = getCurDictView(); // May be proxy view
+ Q_ASSERT( dictView );
+ DictTableModel* dictModel = dictView->dicTableModel(); // Always original model
+ Q_ASSERT( dictModel );
+
+ // Parse records to be pasted
+ DicCsvReader csvReader;
+ CsvImportData params;
+ params.firstLineIsHeader = true;
+ params.commentChar = '#';
+ params.fieldSeparationMode = EFieldSeparatorExactString;
+ params.fieldSeparators = "& ";
+ params.fromLine = 1;
+ params.textDelimiter = '"';
+ params.colsToImport = 0; // All
+ QList<DicRecord*> records = csvReader.readEntries( pastedStr, params );
+ if( records.empty() )
+ return;
+ QStringList pastedFieldNames = csvReader.fieldNames();
+
+ // Check for new pasted fields. Ask user what to do with them.
+ QStringList dicFieldNames = model->curDictionary()->fieldNames();
+ QStringList newFieldNames;
+ foreach( QString name, pastedFieldNames )
+ if( !dicFieldNames.contains( name ) )
+ newFieldNames << name;
+ if( !newFieldNames.isEmpty() )
+ {
+ QMessageBox msgBox;
+ msgBox.setWindowTitle( windowTitle() );
+ bool existingFields = pastedFieldNames.size() - newFieldNames.size() > 0;
+ msgBox.setText( tr("The pasted records contain %n new field(s)", "", newFieldNames.size()) + ":\n" + newFieldNames.join(", ") + "." );
+ msgBox.setInformativeText(tr("Do you want to add new fields to this dictionary?"));
+ msgBox.setIcon( QMessageBox::Question );
+ QPushButton* addFieldsBtn = msgBox.addButton( tr("Add new fields"), QMessageBox::YesRole );
+ QPushButton* pasteExistingBtn = msgBox.addButton( tr("Paste only existing fields"), QMessageBox::NoRole );
+ pasteExistingBtn->setEnabled( existingFields );
+ msgBox.addButton( QMessageBox::Cancel );
+ msgBox.setDefaultButton( addFieldsBtn );
+ int res = msgBox.exec();
+ QAbstractButton* clickedBtn = msgBox.clickedButton();
+ if( clickedBtn == addFieldsBtn )
+ dictModel->addFields( newFieldNames );
+ else if( res == QMessageBox::Cancel ) // do not paste records
+ return;
+ // If paste only existing fields, don't make any changes here, just continue.
+ }
+
+ QUndoCommand* command = new PasteRecordsCmd( records, this ); // Takes ownership of records
+ dictModel->undoStack()->push( command );
+ }
+
+void MainWindow::readSettings()
+{
+ loadGeneralSettings();
+ StudySettings::inst()->load();
+}
+
+void MainWindow::loadGeneralSettings()
+{
+ QSettings settings;
+ move(settings.value("main-pos", QPoint(100, 100)).toPoint());
+ resize(settings.value("main-size", QSize(600, 500)).toSize());
+ if (settings.value("main-maximized", false).toBool())
+ showMaximized();
+
+ restoreState(settings.value("main-state").toByteArray(), 0);
+ QStringList recentFiles = settings.value("main-recent-files").toStringList();
+ recentFilesMan->createRecentFileActs(recentFiles);
+ workPath = settings.value("work-path",
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).
+ toString();
+}
+
+void MainWindow::writeSettings()
+{
+ saveGeneralSettings();
+}
+
+void MainWindow::saveGeneralSettings()
+{
+ QSettings settings;
+ settings.setValue("main-maximized", isMaximized());
+ if(!isMaximized())
+ {
+ settings.setValue("main-pos", pos());
+ settings.setValue("main-size", size());
+ }
+ settings.setValue("main-state", saveState(0));
+ settings.setValue("main-recent-files", recentFilesMan->getFiles());
+ settings.setValue("work-path", workPath);
+ saveSession();
+}
+
+void MainWindow::saveSession()
+{
+ QSettings settings;
+ QStringList sessionFiles;
+ for(int i = 0; i < dictTabWidget->count(); i++)
+ {
+ const Dictionary* dict = model->dictionary(i);
+ sessionFiles << dict->getFilePath();
+ }
+ settings.setValue("session", sessionFiles);
+ settings.setValue("session-cur-tab", model->getCurDictIndex());
+}
+
+void MainWindow::startStudy(int studyType)
+ {
+ int packIx = packsTreeView->currentIndex().row();
+
+ if(studyWindow)
+ {
+ if((int)studyWindow->getStudyType() == studyType)
+ {
+ studyWindow->showNormal();
+ studyWindow->raise();
+ studyWindow->activateWindow();
+ return;
+ }
+ else
+ {
+ disconnect(studyWindow, SIGNAL(destroyed()), this, SLOT(activate()));
+ studyWindow->close();
+ }
+ }
+
+ IStudyModel* studyModel = model->createStudyModel(studyType, packIx);
+ if(!studyModel)
+ {
+ QMessageBox::critical(this, Strings::errorTitle(),
+ tr("The study cannot be started.", "First part of error message") + "\n" +
+ model->getErrorMessage());
+ return;
+ }
+
+ if( studyType == AppModel::SpacedRepetition )
+ connect(studyModel, SIGNAL(nextCardSelected()), SLOT(updatePacksTreeView()));
+
+ // Create study window
+ switch( studyType )
+ {
+ case AppModel::WordDrill:
+ studyWindow = new WordDrillWindow(dynamic_cast<WordDrillModel*>(studyModel), this );
+ break;
+ case AppModel::SpacedRepetition:
+ studyWindow = new SpacedRepetitionWindow(dynamic_cast<SpacedRepetitionModel*>(studyModel), this );
+ break;
+ }
+ connect(studyWindow, SIGNAL(destroyed()), SLOT(activate()));
+ studyWindow->show();
+ }
+
+void MainWindow::startSpacedRepetitionStudy()
+ {
+ startStudy( AppModel::SpacedRepetition );
+ }
+
+void MainWindow::goToDictionaryRecord(const Dictionary* aDictionary, int aRecordRow )
+ {
+ int dicIx = model->indexOfDictionary( const_cast<Dictionary*>( aDictionary ) );
+ if( dicIx < 0 )
+ return;
+ show();
+ raise();
+ activateWindow();
+ dictTabWidget->goToDictionaryRecord( dicIx, aRecordRow );
+ }
+
+void MainWindow::openDictionaryOptions()
+{
+ Dictionary* curDict = model->curDictionary();
+ if( !curDict )
+ return;
+ DictionaryOptionsDialog dicOptions( curDict, this );
+ int res = dicOptions.exec();
+ if (res == QDialog::Accepted)
+ {
+ curDict->setDictConfig( &dicOptions.m_dict );
+ getCurDictView()->dicTableModel()->resetData();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::openFontColorSettings()
+{
+ FontColorSettingsDialog dialog( this );
+ int res = dialog.exec();
+ if (res == QDialog::Accepted)
+ {
+ *(FieldStyleFactory::inst()) = *dialog.styleFactory();
+ FieldStyleFactory::inst()->save();
+ }
+}
+
+void MainWindow::openStudySettings()
+{
+ StudySettingsDialog dialog(this);
+ if(dialog.exec() == QDialog::Accepted)
+ {
+ *(StudySettings::inst()) = dialog.getSettings();
+ StudySettings::inst()->save();
+ }
+}
+
+void MainWindow::setCurDictTab(int index)
+{
+ dictTabWidget->setCurrentIndex(index);
+}
+
+void MainWindow::setCurDictTabByTreeIndex( const QModelIndex& aIndex )
+{
+ if( !aIndex.isValid() )
+ return;
+ dictTabWidget->setCurrentIndex( static_cast<TreeItem*>( aIndex.internalPointer() )->topParentRow() );
+}
+
+// Set the pack selection to the correct dictionary
+void MainWindow::updatePackSelection( int aDic )
+{
+ if( !model->curDictionary() )
+ return;
+ if( aDic == -1 ) // default value
+ aDic = model->curDictionary()->row();
+ QModelIndex curIx = packsTreeView->currentIndex();
+ if( curIx.isValid() && static_cast<TreeItem*>( curIx.internalPointer() )->topParentRow() == aDic )
+ return;
+ QAbstractItemModel* packsTreeModel = packsTreeView->model();
+ QModelIndex dicIx = packsTreeModel->index( aDic, 0, QModelIndex() );
+ int curPackIx;
+ if( !studyWindow.isNull() )
+ curPackIx = model->curCardPackIx();
+ else
+ curPackIx = 0; // first pack
+ QModelIndex packIndex = packsTreeModel->index( curPackIx, 0, dicIx );
+ packsTreeView->selectionModel()->setCurrentIndex( packIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
+}
+
+void MainWindow::showStatistics()
+{
+ StatisticsView(getCurDict()).exec();
+}