#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( 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; icount(); 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(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(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() << 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() << 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() << 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( 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( tableView->model() ); if( proxyModel ) { QModelIndexList srcIndexes; foreach( QModelIndex index, selectedIndexes ) { QModelIndex srcIndex = proxyModel->mapToSource( index ); srcIndexes << srcIndex; } selectedIndexes = srcIndexes; } Dictionary* dict = model->curDictionary(); QList entries; foreach( QModelIndex index, selectedIndexes ) entries << const_cast(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 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(studyModel), this ); break; case AppModel::SpacedRepetition: studyWindow = new SpacedRepetitionWindow(dynamic_cast(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( 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( 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( 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(); }