diff options
author | Jedidiah Barber <contact@jedbarber.id.au> | 2021-07-14 11:49:10 +1200 |
---|---|---|
committer | Jedidiah Barber <contact@jedbarber.id.au> | 2021-07-14 11:49:10 +1200 |
commit | d24f813f3f2a05c112e803e4256b53535895fc98 (patch) | |
tree | 601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9 /src/main-view/MainWindow.cpp |
Diffstat (limited to 'src/main-view/MainWindow.cpp')
-rw-r--r-- | src/main-view/MainWindow.cpp | 1313 |
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 © ..."), 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(); +} |