summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
committerJedidiah Barber <contact@jedbarber.id.au>2021-07-14 11:49:10 +1200
commitd24f813f3f2a05c112e803e4256b53535895fc98 (patch)
tree601e6ae9a1cd44bcfdcf91739a5ca36aedd827c9
Initial mirror commitHEADmaster
-rw-r--r--.gitignore22
-rw-r--r--AUTHORS4
-rw-r--r--COPYING675
-rw-r--r--ChangeLog211
-rw-r--r--Jenkinsfile36
-rw-r--r--README244
-rw-r--r--application.qrc59
-rwxr-xr-xcopy-docs.sh9
-rw-r--r--doc/chart.xcfbin0 -> 83901 bytes
-rw-r--r--doc/config/freshmemory.ini59
-rw-r--r--doc/config/install.txt23
-rw-r--r--doc/dict-format.txt147
-rw-r--r--doc/file-associations.txt29
-rw-r--r--doc/pie-chart.xcfbin0 -> 94110 bytes
-rw-r--r--doc/study-format.txt169
-rw-r--r--examples/countries-europe-2.fmd259
-rw-r--r--examples/countries-europe-2/Albania.pngbin0 -> 3345 bytes
-rw-r--r--examples/countries-europe-2/Andorra.pngbin0 -> 3337 bytes
-rw-r--r--examples/countries-europe-2/Austria.pngbin0 -> 1682 bytes
-rw-r--r--examples/countries-europe-2/Belarus.pngbin0 -> 2948 bytes
-rw-r--r--examples/countries-europe-2/Belgium.pngbin0 -> 1892 bytes
-rw-r--r--examples/countries-europe-2/Bosnia & Herzegovina.pngbin0 -> 4101 bytes
-rw-r--r--examples/countries-europe-2/Bulgaria.pngbin0 -> 1579 bytes
-rw-r--r--examples/countries-europe-2/Croatia.pngbin0 -> 3270 bytes
-rw-r--r--examples/countries-europe-2/Cyprus.pngbin0 -> 2485 bytes
-rw-r--r--examples/countries-europe-2/Czech Republic.pngbin0 -> 2130 bytes
-rw-r--r--examples/countries-europe-2/Denmark.pngbin0 -> 1803 bytes
-rw-r--r--examples/countries-europe-2/Estonia.pngbin0 -> 1755 bytes
-rw-r--r--examples/countries-europe-2/Finland.pngbin0 -> 1759 bytes
-rw-r--r--examples/countries-europe-2/France.pngbin0 -> 1851 bytes
-rw-r--r--examples/countries-europe-2/Germany.pngbin0 -> 1729 bytes
-rw-r--r--examples/countries-europe-2/Great Britain.pngbin0 -> 5224 bytes
-rw-r--r--examples/countries-europe-2/Greece.pngbin0 -> 2168 bytes
-rw-r--r--examples/countries-europe-2/Greenland.pngbin0 -> 2497 bytes
-rw-r--r--examples/countries-europe-2/Hungary.pngbin0 -> 1618 bytes
-rw-r--r--examples/countries-europe-2/Iceland.pngbin0 -> 2002 bytes
-rw-r--r--examples/countries-europe-2/Ireland.pngbin0 -> 1804 bytes
-rw-r--r--examples/countries-europe-2/Italy.pngbin0 -> 1798 bytes
-rw-r--r--examples/countries-europe-2/Latvia.pngbin0 -> 1658 bytes
-rw-r--r--examples/countries-europe-2/Liechtenstein.pngbin0 -> 2525 bytes
-rw-r--r--examples/countries-europe-2/Lithuania.pngbin0 -> 1717 bytes
-rw-r--r--examples/countries-europe-2/Luxembourg.pngbin0 -> 1635 bytes
-rw-r--r--examples/countries-europe-2/Macedonia.pngbin0 -> 4933 bytes
-rw-r--r--examples/countries-europe-2/Malta.pngbin0 -> 2030 bytes
-rw-r--r--examples/countries-europe-2/Moldova.pngbin0 -> 3302 bytes
-rw-r--r--examples/countries-europe-2/Monaco.pngbin0 -> 1705 bytes
-rw-r--r--examples/countries-europe-2/Montenegro.pngbin0 -> 3496 bytes
-rw-r--r--examples/countries-europe-2/Netherlands.pngbin0 -> 1602 bytes
-rw-r--r--examples/countries-europe-2/Norway.pngbin0 -> 2012 bytes
-rw-r--r--examples/countries-europe-2/Poland.pngbin0 -> 1353 bytes
-rw-r--r--examples/countries-europe-2/Portugal.pngbin0 -> 3583 bytes
-rw-r--r--examples/countries-europe-2/Romania.pngbin0 -> 1886 bytes
-rw-r--r--examples/countries-europe-2/Russia.pngbin0 -> 1558 bytes
-rw-r--r--examples/countries-europe-2/San Marino.pngbin0 -> 4266 bytes
-rw-r--r--examples/countries-europe-2/Serbia.pngbin0 -> 3301 bytes
-rw-r--r--examples/countries-europe-2/Slovakia.pngbin0 -> 2857 bytes
-rw-r--r--examples/countries-europe-2/Slovenia.pngbin0 -> 2350 bytes
-rw-r--r--examples/countries-europe-2/Spain.pngbin0 -> 2911 bytes
-rw-r--r--examples/countries-europe-2/Sweden.pngbin0 -> 1970 bytes
-rw-r--r--examples/countries-europe-2/Switzerland.pngbin0 -> 1782 bytes
-rw-r--r--examples/countries-europe-2/Ukraine.pngbin0 -> 1817 bytes
-rw-r--r--freshmemory.pro262
-rw-r--r--freshmemory.rc1
-rw-r--r--images/1downarrow.pngbin0 -> 722 bytes
-rw-r--r--images/1leftarrow.pngbin0 -> 832 bytes
-rw-r--r--images/1rightarrow.pngbin0 -> 807 bytes
-rw-r--r--images/1uparrow.pngbin0 -> 787 bytes
-rw-r--r--images/Aa.pngbin0 -> 762 bytes
-rw-r--r--images/RX.pngbin0 -> 761 bytes
-rw-r--r--images/add-image.pngbin0 -> 1118 bytes
-rw-r--r--images/add.pngbin0 -> 1573 bytes
-rw-r--r--images/attic/clock.pngbin0 -> 21387 bytes
-rw-r--r--images/attic/large-arrow-left.pngbin0 -> 25627 bytes
-rw-r--r--images/attic/large-arrow-right.pngbin0 -> 25924 bytes
-rw-r--r--images/back.pngbin0 -> 1625 bytes
-rw-r--r--images/blue-triangle-down.pngbin0 -> 1024 bytes
-rw-r--r--images/broken-image.pngbin0 -> 654 bytes
-rw-r--r--images/chart-future.pngbin0 -> 5563 bytes
-rw-r--r--images/chart-past.pngbin0 -> 5865 bytes
-rw-r--r--images/continue-search.pngbin0 -> 5845 bytes
-rw-r--r--images/delete.pngbin0 -> 1607 bytes
-rw-r--r--images/dic-options.pngbin0 -> 2675 bytes
-rw-r--r--images/down.pngbin0 -> 1604 bytes
-rw-r--r--images/editcopy.pngbin0 -> 879 bytes
-rw-r--r--images/editcut.pngbin0 -> 2169 bytes
-rw-r--r--images/editpaste.pngbin0 -> 1458 bytes
-rw-r--r--images/exit.pngbin0 -> 1915 bytes
-rw-r--r--images/fields.pngbin0 -> 1673 bytes
-rw-r--r--images/filenew.pngbin0 -> 1369 bytes
-rw-r--r--images/fileopen.pngbin0 -> 2232 bytes
-rw-r--r--images/filesave.pngbin0 -> 1348 bytes
-rw-r--r--images/filesaveas.pngbin0 -> 2069 bytes
-rw-r--r--images/find.pngbin0 -> 1553 bytes
-rw-r--r--images/flashcards-24.pngbin0 -> 1299 bytes
-rw-r--r--images/font-style.pngbin0 -> 2185 bytes
-rw-r--r--images/forward.pngbin0 -> 1647 bytes
-rw-r--r--images/freshmemory.pngbin0 -> 27419 bytes
-rw-r--r--images/freshmemory.svg165
-rw-r--r--images/gplv3-88x31.pngbin0 -> 2666 bytes
-rw-r--r--images/gray-cross.pngbin0 -> 646 bytes
-rw-r--r--images/green-tick.pngbin0 -> 898 bytes
-rw-r--r--images/green-triangle-up.pngbin0 -> 971 bytes
-rw-r--r--images/info.pngbin0 -> 2362 bytes
-rw-r--r--images/language.pngbin0 -> 6948 bytes
-rw-r--r--images/mainicon.icobin0 -> 16958 bytes
-rw-r--r--images/new-topright.pngbin0 -> 4032 bytes
-rw-r--r--images/next.pngbin0 -> 2138 bytes
-rw-r--r--images/openbook-24.pngbin0 -> 1616 bytes
-rw-r--r--images/orig/Aa.xcfbin0 -> 5484 bytes
-rw-r--r--images/orig/RX.xcfbin0 -> 8022 bytes
-rw-r--r--images/orig/blue-triangle-down.xcfbin0 -> 3862 bytes
-rw-r--r--images/orig/card.xcfbin0 -> 1600 bytes
-rw-r--r--images/orig/chart-future.xcfbin0 -> 12868 bytes
-rw-r--r--images/orig/chart-past.xcfbin0 -> 12451 bytes
-rw-r--r--images/orig/green-triangle-up.xcfbin0 -> 3726 bytes
-rw-r--r--images/orig/question.xcfbin0 -> 3555 bytes
-rw-r--r--images/orig/selection.xcfbin0 -> 7370 bytes
-rw-r--r--images/orig/spaced-rep.xcfbin0 -> 4351 bytes
-rw-r--r--images/orig/whole-words.xcfbin0 -> 3436 bytes
-rw-r--r--images/orig/word-drill.xcfbin0 -> 2467 bytes
-rw-r--r--images/passes.pngbin0 -> 888 bytes
-rw-r--r--images/pencil.pngbin0 -> 1296 bytes
-rw-r--r--images/pie-chart-3d.pngbin0 -> 8979 bytes
-rw-r--r--images/question.pngbin0 -> 1035 bytes
-rw-r--r--images/red-cross.pngbin0 -> 1731 bytes
-rw-r--r--images/red-stop.pngbin0 -> 2132 bytes
-rw-r--r--images/remove.pngbin0 -> 2158 bytes
-rw-r--r--images/selection.pngbin0 -> 534 bytes
-rw-r--r--images/spaced-rep.pngbin0 -> 1333 bytes
-rw-r--r--images/statistics.pngbin0 -> 942 bytes
-rw-r--r--images/study-settings.pngbin0 -> 2760 bytes
-rw-r--r--images/up.pngbin0 -> 1595 bytes
-rw-r--r--images/warning.pngbin0 -> 1692 bytes
-rw-r--r--images/whole-words.pngbin0 -> 344 bytes
-rw-r--r--images/word-drill.pngbin0 -> 493 bytes
-rw-r--r--packaging/FileAssociation.nsh190
-rwxr-xr-xpackaging/clean-deb.sh4
-rwxr-xr-xpackaging/create-source-archive.sh19
-rw-r--r--packaging/debian/changelog.Debian3
-rw-r--r--packaging/debian/conffiles1
-rw-r--r--packaging/debian/control17
-rw-r--r--packaging/debian/copyright9
-rw-r--r--packaging/debian/freshmemory.113
-rw-r--r--packaging/debian/freshmemory.desktop15
-rw-r--r--packaging/debian/freshmemory.xml8
-rwxr-xr-xpackaging/debian/postinst6
-rwxr-xr-xpackaging/debian/prerm7
-rw-r--r--packaging/freshmemory.nsi72
-rwxr-xr-xpackaging/make-deb.sh126
-rw-r--r--packaging/make-wininstaller.bat15
-rw-r--r--packaging/packaging.txt66
-rw-r--r--packaging/qt-win/cleanup.bat2
-rw-r--r--packaging/qt-win/copy_qtdlls.bat28
-rw-r--r--packaging/qt-win/list.txt25
-rw-r--r--releases/1.0.1/README61
-rw-r--r--releases/1.0.1/news.txt1
-rw-r--r--releases/1.0.2/README17
-rw-r--r--releases/1.0.2/news.txt1
-rw-r--r--releases/1.1/README58
-rw-r--r--releases/1.1/news.txt1
-rw-r--r--releases/1.2/README27
-rw-r--r--releases/1.2/news.txt2
-rw-r--r--releases/1.4.4/news.txt3
-rw-r--r--releases/1.4.5/news.txt2
-rw-r--r--releases/README74
-rw-r--r--releases/screenshots/fm-1.0-e.pngbin0 -> 36991 bytes
-rw-r--r--releases/screenshots/fm-1.0-f.pngbin0 -> 34938 bytes
-rw-r--r--releases/screenshots/fm-1.1-a.pngbin0 -> 57121 bytes
-rw-r--r--releases/screenshots/fm-1.1-b.pngbin0 -> 28994 bytes
-rw-r--r--releases/screenshots/fm-1.1-c.pngbin0 -> 29042 bytes
-rw-r--r--releases/screenshots/fm-1.1-d.pngbin0 -> 47246 bytes
-rw-r--r--releases/screenshots/fm-1.1-e.pngbin0 -> 78414 bytes
-rw-r--r--releases/screenshots/names.txt8
-rw-r--r--src/charts/Chart.cpp23
-rw-r--r--src/charts/Chart.h32
-rw-r--r--src/charts/ChartAxes.cpp138
-rw-r--r--src/charts/ChartAxes.h51
-rw-r--r--src/charts/ChartDataLine.cpp80
-rw-r--r--src/charts/ChartDataLine.h37
-rw-r--r--src/charts/ChartMarker.cpp50
-rw-r--r--src/charts/ChartMarker.h36
-rw-r--r--src/charts/ChartScene.cpp111
-rw-r--r--src/charts/ChartScene.h55
-rw-r--r--src/charts/ChartToolTip.cpp43
-rw-r--r--src/charts/ChartToolTip.h26
-rw-r--r--src/charts/ChartView.cpp12
-rw-r--r--src/charts/ChartView.h16
-rw-r--r--src/charts/DataPoint.h15
-rw-r--r--src/charts/PieChart.cpp28
-rw-r--r--src/charts/PieChart.h27
-rw-r--r--src/charts/PieChartScene.cpp30
-rw-r--r--src/charts/PieChartScene.h30
-rw-r--r--src/charts/PieLegend.cpp48
-rw-r--r--src/charts/PieLegend.h34
-rw-r--r--src/charts/PieRound.cpp77
-rw-r--r--src/charts/PieRound.h37
-rw-r--r--src/charts/TimeChart.cpp155
-rw-r--r--src/charts/TimeChart.h54
-rw-r--r--src/dic-options/DictionaryOptionsDialog.cpp62
-rw-r--r--src/dic-options/DictionaryOptionsDialog.h41
-rw-r--r--src/dic-options/DraggableListModel.cpp83
-rw-r--r--src/dic-options/DraggableListModel.h33
-rw-r--r--src/dic-options/FieldStyleDelegate.cpp35
-rw-r--r--src/dic-options/FieldStyleDelegate.h19
-rw-r--r--src/dic-options/FieldsListModel.cpp222
-rw-r--r--src/dic-options/FieldsListModel.h49
-rw-r--r--src/dic-options/FieldsPage.cpp139
-rw-r--r--src/dic-options/FieldsPage.h37
-rw-r--r--src/dic-options/FieldsPreviewModel.cpp34
-rw-r--r--src/dic-options/FieldsPreviewModel.h27
-rw-r--r--src/dic-options/FieldsView.cpp41
-rw-r--r--src/dic-options/FieldsView.h24
-rw-r--r--src/dic-options/PackFieldsListModel.cpp101
-rw-r--r--src/dic-options/PackFieldsListModel.h34
-rw-r--r--src/dic-options/PackFieldsView.cpp36
-rw-r--r--src/dic-options/PackFieldsView.h24
-rw-r--r--src/dic-options/PacksListModel.cpp81
-rw-r--r--src/dic-options/PacksListModel.h32
-rw-r--r--src/dic-options/PacksPage.cpp345
-rw-r--r--src/dic-options/PacksPage.h70
-rw-r--r--src/dic-options/UnusedFieldsListModel.cpp111
-rw-r--r--src/dic-options/UnusedFieldsListModel.h35
-rw-r--r--src/dictionary/Card.cpp125
-rw-r--r--src/dictionary/Card.h50
-rw-r--r--src/dictionary/CardPack.cpp432
-rw-r--r--src/dictionary/CardPack.h144
-rw-r--r--src/dictionary/DicCsvReader.cpp205
-rw-r--r--src/dictionary/DicCsvReader.h41
-rw-r--r--src/dictionary/DicCsvWriter.cpp110
-rw-r--r--src/dictionary/DicCsvWriter.h31
-rw-r--r--src/dictionary/DicRecord.cpp62
-rw-r--r--src/dictionary/DicRecord.h40
-rw-r--r--src/dictionary/Dictionary.cpp601
-rw-r--r--src/dictionary/Dictionary.h187
-rw-r--r--src/dictionary/DictionaryReader.cpp384
-rw-r--r--src/dictionary/DictionaryReader.h44
-rw-r--r--src/dictionary/DictionaryWriter.cpp79
-rw-r--r--src/dictionary/DictionaryWriter.h26
-rw-r--r--src/dictionary/Field.cpp18
-rw-r--r--src/dictionary/Field.h46
-rw-r--r--src/dictionary/ICardPack.cpp23
-rw-r--r--src/dictionary/ICardPack.h33
-rw-r--r--src/dictionary/IDictionary.cpp59
-rw-r--r--src/dictionary/IDictionary.h51
-rw-r--r--src/dictionary/TreeItem.h25
-rw-r--r--src/export-import/CsvData.h36
-rw-r--r--src/export-import/CsvDialog.cpp176
-rw-r--r--src/export-import/CsvDialog.h62
-rw-r--r--src/export-import/CsvExportDialog.cpp137
-rw-r--r--src/export-import/CsvExportDialog.h52
-rw-r--r--src/export-import/CsvImportDialog.cpp155
-rw-r--r--src/export-import/CsvImportDialog.h64
-rw-r--r--src/field-styles/FieldStyle.cpp38
-rw-r--r--src/field-styles/FieldStyle.h28
-rw-r--r--src/field-styles/FieldStyleFactory.cpp145
-rw-r--r--src/field-styles/FieldStyleFactory.h49
-rw-r--r--src/main-view/AboutDialog.cpp27
-rw-r--r--src/main-view/AboutDialog.h17
-rw-r--r--src/main-view/AppModel.cpp196
-rw-r--r--src/main-view/AppModel.h64
-rw-r--r--src/main-view/CardFilterModel.cpp24
-rw-r--r--src/main-view/CardFilterModel.h21
-rw-r--r--src/main-view/CardPreview.cpp55
-rw-r--r--src/main-view/CardPreview.h30
-rw-r--r--src/main-view/DictTableDelegate.cpp128
-rw-r--r--src/main-view/DictTableDelegate.h44
-rw-r--r--src/main-view/DictTableDelegatePainter.cpp61
-rw-r--r--src/main-view/DictTableDelegatePainter.h36
-rw-r--r--src/main-view/DictTableModel.cpp142
-rw-r--r--src/main-view/DictTableModel.h52
-rw-r--r--src/main-view/DictTableView.cpp84
-rw-r--r--src/main-view/DictTableView.h36
-rw-r--r--src/main-view/DictionaryTabWidget.cpp108
-rw-r--r--src/main-view/DictionaryTabWidget.h46
-rw-r--r--src/main-view/FieldContentCodec.cpp52
-rw-r--r--src/main-view/FieldContentCodec.h28
-rw-r--r--src/main-view/FieldContentPainter.h16
-rw-r--r--src/main-view/FindPanel.cpp258
-rw-r--r--src/main-view/FindPanel.h66
-rw-r--r--src/main-view/LanguageMenu.cpp64
-rw-r--r--src/main-view/LanguageMenu.h28
-rw-r--r--src/main-view/MainWindow.cpp1313
-rw-r--r--src/main-view/MainWindow.h218
-rw-r--r--src/main-view/PacksTreeModel.cpp93
-rw-r--r--src/main-view/PacksTreeModel.h32
-rw-r--r--src/main-view/RecentFilesManager.cpp99
-rw-r--r--src/main-view/RecentFilesManager.h43
-rw-r--r--src/main-view/RecordEditor.cpp122
-rw-r--r--src/main-view/RecordEditor.h46
-rw-r--r--src/main-view/UndoCommands.cpp322
-rw-r--r--src/main-view/UndoCommands.h109
-rw-r--r--src/main-view/WelcomeScreen.cpp51
-rw-r--r--src/main-view/WelcomeScreen.h25
-rw-r--r--src/main.cpp102
-rw-r--r--src/main.h8
-rw-r--r--src/settings/ColorBox.cpp36
-rw-r--r--src/settings/ColorBox.h28
-rw-r--r--src/settings/FontColorSettingsDialog.cpp300
-rw-r--r--src/settings/FontColorSettingsDialog.h71
-rw-r--r--src/settings/StudySettingsDialog.cpp141
-rw-r--r--src/settings/StudySettingsDialog.h45
-rw-r--r--src/settings/StylePreviewModel.cpp63
-rw-r--r--src/settings/StylePreviewModel.h32
-rw-r--r--src/settings/StylesListModel.h16
-rw-r--r--src/statistics/BaseStatPage.cpp38
-rw-r--r--src/statistics/BaseStatPage.h31
-rw-r--r--src/statistics/ProgressPage.cpp41
-rw-r--r--src/statistics/ProgressPage.h33
-rw-r--r--src/statistics/ScheduledPage.cpp35
-rw-r--r--src/statistics/ScheduledPage.h22
-rw-r--r--src/statistics/StatisticsParams.cpp2
-rw-r--r--src/statistics/StatisticsParams.h20
-rw-r--r--src/statistics/StatisticsView.cpp182
-rw-r--r--src/statistics/StatisticsView.h53
-rw-r--r--src/statistics/StudiedPage.cpp17
-rw-r--r--src/statistics/StudiedPage.h18
-rw-r--r--src/statistics/TimeChartPage.cpp54
-rw-r--r--src/statistics/TimeChartPage.h33
-rw-r--r--src/strings.cpp6
-rw-r--r--src/strings.h22
-rw-r--r--src/study/CardEditDialog.cpp119
-rw-r--r--src/study/CardEditDialog.h42
-rw-r--r--src/study/CardSideView.cpp205
-rw-r--r--src/study/CardSideView.h52
-rw-r--r--src/study/CardsStatusBar.cpp82
-rw-r--r--src/study/CardsStatusBar.h37
-rw-r--r--src/study/IStudyModel.cpp9
-rw-r--r--src/study/IStudyModel.h35
-rw-r--r--src/study/IStudyWindow.cpp293
-rw-r--r--src/study/IStudyWindow.h107
-rw-r--r--src/study/NumberFrame.cpp40
-rw-r--r--src/study/NumberFrame.h29
-rw-r--r--src/study/SpacedRepetitionModel.cpp390
-rw-r--r--src/study/SpacedRepetitionModel.h79
-rw-r--r--src/study/SpacedRepetitionWindow.cpp380
-rw-r--r--src/study/SpacedRepetitionWindow.h89
-rw-r--r--src/study/StudyFileReader.cpp238
-rw-r--r--src/study/StudyFileReader.h44
-rw-r--r--src/study/StudyFileWriter.cpp68
-rw-r--r--src/study/StudyFileWriter.h26
-rw-r--r--src/study/StudyRecord.cpp129
-rw-r--r--src/study/StudyRecord.h62
-rw-r--r--src/study/StudySettings.cpp93
-rw-r--r--src/study/StudySettings.h40
-rw-r--r--src/study/WarningPanel.cpp49
-rw-r--r--src/study/WarningPanel.h25
-rw-r--r--src/study/WordDrillModel.cpp163
-rw-r--r--src/study/WordDrillModel.h62
-rw-r--r--src/study/WordDrillWindow.cpp180
-rw-r--r--src/study/WordDrillWindow.h46
-rw-r--r--src/utils/IRandomGenerator.h17
-rw-r--r--src/utils/RandomGenerator.cpp38
-rw-r--r--src/utils/RandomGenerator.h16
-rw-r--r--src/utils/TimeProvider.cpp6
-rw-r--r--src/utils/TimeProvider.h12
-rw-r--r--src/version.cpp3
-rw-r--r--src/version.h19
-rw-r--r--tests/common/RecordsParam.cpp51
-rw-r--r--tests/common/RecordsParam.h58
-rw-r--r--tests/common/RecordsParam_create.cpp62
-rw-r--r--tests/common/printQtTypes.cpp31
-rw-r--r--tests/common/printQtTypes.h23
-rw-r--r--tests/fute/charts/charts.pro35
-rw-r--r--tests/fute/charts/charts_test.cpp52
-rw-r--r--tests/fute/charts/charts_test.h31
-rw-r--r--tests/fute/charts/main.cpp12
-rw-r--r--tests/fute/pieCharts/main.cpp12
-rw-r--r--tests/fute/pieCharts/pieCharts.pro29
-rw-r--r--tests/fute/pieCharts/pieCharts_test.cpp52
-rw-r--r--tests/fute/pieCharts/pieCharts_test.h34
-rw-r--r--tests/fute/timeCharts/main.cpp12
-rw-r--r--tests/fute/timeCharts/timeCharts.pro35
-rw-r--r--tests/fute/timeCharts/timeCharts_test.cpp58
-rw-r--r--tests/fute/timeCharts/timeCharts_test.h30
-rw-r--r--tests/mocks/CardPack_mock.cpp31
-rw-r--r--tests/mocks/CardPack_mock.h23
-rw-r--r--tests/mocks/Dictionary_mock.cpp6
-rw-r--r--tests/mocks/Dictionary_mock.h32
-rw-r--r--tests/mocks/RandomGenerator_mock.h33
-rw-r--r--tests/mocks/TimeProvider_mock.cpp9
-rw-r--r--tests/unit/Card/Card_GenerateAnswers_test.cpp66
-rw-r--r--tests/unit/Card/Card_GenerateAnswers_test.h34
-rw-r--r--tests/unit/Card/Card_test.cpp36
-rw-r--r--tests/unit/Card/Card_test.h18
-rw-r--r--tests/unit/Card/Card_test_QuestionAnswer.cpp30
-rw-r--r--tests/unit/Card/card.pri10
-rw-r--r--tests/unit/CardPack/CardPack_GenerateCards_test.cpp43
-rw-r--r--tests/unit/CardPack/CardPack_GenerateCards_test.h26
-rw-r--r--tests/unit/CardPack/CardPack_test.cpp80
-rw-r--r--tests/unit/CardPack/CardPack_test.h31
-rw-r--r--tests/unit/CardPack/cPack.pri7
-rw-r--r--tests/unit/CardSideView/CardSideView_test.cpp49
-rw-r--r--tests/unit/CardSideView/CardSideView_test.h21
-rw-r--r--tests/unit/CardSideView/csView.pri8
-rw-r--r--tests/unit/RandomGenerator/RandomGenerator_test.cpp79
-rw-r--r--tests/unit/RandomGenerator/rndGen.pri6
-rw-r--r--tests/unit/Settings/FieldStyleFactory_test.cpp88
-rw-r--r--tests/unit/Settings/FieldStyleFactory_test.h29
-rw-r--r--tests/unit/Settings/StudySettings_test.cpp42
-rw-r--r--tests/unit/Settings/StudySettings_test.h22
-rw-r--r--tests/unit/Settings/TestSettings.cpp16
-rw-r--r--tests/unit/Settings/TestSettings.h11
-rw-r--r--tests/unit/Settings/set.pri10
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.cpp420
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.h27
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_schedule_test.cpp304
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_schedule_test.h24
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.cpp36
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.h14
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_test.cpp67
-rw-r--r--tests/unit/SpacedRepetitionModel/SRModel_test.h46
-rw-r--r--tests/unit/SpacedRepetitionModel/srModel.pri16
-rw-r--r--tests/unit/cards.pri24
-rw-r--r--tests/unit/common.pri3
-rw-r--r--tests/unit/main.cpp9
-rw-r--r--tests/unit/random.pri1
-rw-r--r--tests/unit/studySets.pri9
-rw-r--r--tests/unit/unit_tests.pro29
-rw-r--r--tr/freshmemory_cs.ts1505
-rw-r--r--tr/freshmemory_de.ts1501
-rw-r--r--tr/freshmemory_en.ts1495
-rw-r--r--tr/freshmemory_es.ts1502
-rw-r--r--tr/freshmemory_fi.ts1498
-rw-r--r--tr/freshmemory_fr.ts1500
-rw-r--r--tr/freshmemory_ru.ts1507
-rw-r--r--tr/freshmemory_uk.ts1504
-rw-r--r--userdocs/_static/default.css498
-rw-r--r--userdocs/_static/icon-note.pngbin0 -> 2040 bytes
-rw-r--r--userdocs/_static/icon-tip.pngbin0 -> 3320 bytes
-rw-r--r--userdocs/_templates/layout.html10
-rw-r--r--userdocs/_templates/version.html2
-rw-r--r--userdocs/about.md28
-rw-r--r--userdocs/activation.rst92
-rw-r--r--userdocs/browsing-cards.rst65
-rw-r--r--userdocs/cards-generation.rst30
-rw-r--r--userdocs/conf.py259
-rw-r--r--userdocs/diagrams/cards_generation.diabin0 -> 2344 bytes
-rw-r--r--userdocs/diagrams/records.pngbin0 -> 2949 bytes
-rw-r--r--userdocs/dictionary-and-study.rst29
-rw-r--r--userdocs/dictionary-options.rst90
-rw-r--r--userdocs/icons/1downarrow.pngbin0 -> 722 bytes
-rw-r--r--userdocs/icons/1uparrow.pngbin0 -> 787 bytes
-rw-r--r--userdocs/icons/add.pngbin0 -> 1573 bytes
-rw-r--r--userdocs/icons/delete.pngbin0 -> 1607 bytes
-rw-r--r--userdocs/icons/down.pngbin0 -> 1604 bytes
-rw-r--r--userdocs/icons/filenew.pngbin0 -> 1369 bytes
-rw-r--r--userdocs/icons/filesave.pngbin0 -> 1348 bytes
-rw-r--r--userdocs/icons/passes.pngbin0 -> 888 bytes
-rw-r--r--userdocs/icons/pencil.pngbin0 -> 1296 bytes
-rw-r--r--userdocs/icons/red-cross.pngbin0 -> 1731 bytes
-rw-r--r--userdocs/icons/up.pngbin0 -> 1595 bytes
-rwxr-xr-xuserdocs/images/activation/about_activated.pngbin0 -> 60505 bytes
-rwxr-xr-xuserdocs/images/activation/about_trial.pngbin0 -> 61417 bytes
-rwxr-xr-xuserdocs/images/activation/activation_dialog.pngbin0 -> 14943 bytes
-rwxr-xr-xuserdocs/images/activation/activation_dialog_activated.pngbin0 -> 15294 bytes
-rwxr-xr-xuserdocs/images/activation/activation_dialog_trial.pngbin0 -> 14714 bytes
-rwxr-xr-xuserdocs/images/activation/trial_reminder.pngbin0 -> 21986 bytes
-rw-r--r--userdocs/images/adding_image.pngbin0 -> 14337 bytes
-rw-r--r--userdocs/images/browsing.pngbin0 -> 63411 bytes
-rw-r--r--userdocs/images/browsing2.pngbin0 -> 79382 bytes
-rw-r--r--userdocs/images/cards_generation.pngbin0 -> 16442 bytes
-rw-r--r--userdocs/images/settings/field_options.pngbin0 -> 41689 bytes
-rw-r--r--userdocs/images/settings/field_style_options.pngbin0 -> 41600 bytes
-rw-r--r--userdocs/images/settings/keyword.pngbin0 -> 15399 bytes
-rw-r--r--userdocs/images/settings/pack_options.pngbin0 -> 44392 bytes
-rwxr-xr-xuserdocs/images/settings/study_settings.pngbin0 -> 20556 bytes
-rw-r--r--userdocs/images/spacedrep/edit_card.pngbin0 -> 19802 bytes
-rw-r--r--userdocs/images/spacedrep/new_card.pngbin0 -> 28182 bytes
-rwxr-xr-xuserdocs/images/spacedrep/progress_tooltip.pngbin0 -> 6306 bytes
-rw-r--r--userdocs/images/spacedrep/settings_exact_answer.pngbin0 -> 46651 bytes
-rw-r--r--userdocs/images/spacedrep/spacedrep.pngbin0 -> 26449 bytes
-rw-r--r--userdocs/images/spacedrep/spacedrep_exact_answer.pngbin0 -> 26470 bytes
-rw-r--r--userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.pngbin0 -> 27089 bytes
-rw-r--r--userdocs/images/spacedrep/spacedrep_hidden_answer.pngbin0 -> 26845 bytes
-rw-r--r--userdocs/images/spacedrep/study_progress.pngbin0 -> 3558 bytes
-rw-r--r--userdocs/images/ss-new_dictionary.pngbin0 -> 7451 bytes
-rw-r--r--userdocs/images/ss-word_drill_back_enabled.pngbin0 -> 11861 bytes
-rw-r--r--userdocs/images/ss-word_drill_back_pressed.pngbin0 -> 11282 bytes
-rw-r--r--userdocs/images/ss-word_drill_history.pngbin0 -> 23495 bytes
-rw-r--r--userdocs/images/ss-word_drill_second_cycle.pngbin0 -> 5095 bytes
-rw-r--r--userdocs/images/stats/chart_tooltip.pngbin0 -> 8128 bytes
-rw-r--r--userdocs/images/stats/stats_progress.pngbin0 -> 46119 bytes
-rw-r--r--userdocs/images/stats/stats_scheduled.pngbin0 -> 46220 bytes
-rw-r--r--userdocs/images/stats/stats_studied.pngbin0 -> 50293 bytes
-rw-r--r--userdocs/images/welcome_panel.pngbin0 -> 22740 bytes
-rw-r--r--userdocs/images/word_drill.pngbin0 -> 30388 bytes
-rw-r--r--userdocs/index.rst19
-rw-r--r--userdocs/introduction.rst97
-rw-r--r--userdocs/statistics.rst42
-rw-r--r--userdocs/study-settings.rst51
-rw-r--r--userdocs/studying-cards.rst223
-rw-r--r--version.txt1
492 files changed, 34992 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8c46aeb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+Makefile
+*.cproject
+freshmemory
+*.exe
+*.dll
+*.qm
+*.pro.user
+doc/doxy
+*.orig
+moc_*
+*.o
+qrc_*
+*.Debug
+*.Release
+*.project
+*.settings
+tests/unit/unittest
+userdocs/_build
+.qmake.*
+packaging/debian/*.diff
+freshmemory_*/
+*.deb
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..39a8ea2
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,4 @@
+Fresh Memory is developed by
+Mykhaylo Kopytonenko
+<mishakop AT gmail com>
+
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..10926e8
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,675 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..bdccb33
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,211 @@
+Changelog
+
+ 1.4.2
+==================================
+
+* Bug fix: Incorrect loading of study data.
+* Translation updates
+
+
+ 1.4.1
+==================================
+
+Major changes:
+
+* Spaced repetition algorithm: Don't add new cards, if there are too many scheduled cards for today. Add this number as a configurable study setting.
+* Autosave study file each 3 minutes.
+
+Minor changes:
+
+* Spaced repetition algorithm: One-day-old learning cards are promoted directly to normal learned cards, if the user gives a positive grade.
+* Spaced repetition window: Improve look of the statistic number boxes.
+* Card answer view: add a small spacing between the first answer and the others
+* Study settings dialog: improve layout
+
+Bug fixes:
+
+* Spaced repetition window: Fix drawing of the last section of the study progress bar.
+* Spaced repetition window: Fix shortcut for button "4".
+
+Windows platform:
+
+* Spaced repetition window: improve font of the grade buttons
+
+
+ 1.4.0
+==================================
+
+* Added "Exact answer" mode
+* New look of the grade buttons (Spaced repetition window)
+* Removed grade 2 "Not completely correct".
+* New cards are shown with "New" label
+* Introduced learning steps for new cards: must be repeated 2 times at the first day
+* Available grade buttons depend on the status of the shown card:
+ - New cards have just "OK" and "Easy" grades
+ - Learning cards do not have "Difficult" grade
+* For incorrect cards, replaced card-based intervals with short time intervals: 20 sec and 1 min
+* New look of the study progress bar (Spaced repetition window)
+* New statistic page "Study progress" - studied, scheduled for today and future cards
+* Current session is saved on exit, and reloaded at startup
+* Example dictionaries are available online. Not included in the installation.
+* Online documentation describes used Spaced repetition algorithm in detail
+
+Activation:
+
+* Trial version uses a Product Key, which is available at the web site.
+* One commercial license allows installation on 3 computers
+* Removed offline activation mode
+
+
+ 1.3.0
+==================================
+
+* Added statistics view: studied and scheduled cards
+* Added trial version
+
+
+ 1.2.0
+==================================
+
+* Added card preview for dictionary records
+* Images:
+ - Display image thumbnails in dictionary view (instead of plain HTML tags)
+ - Add images with GUI
+* New study setting: Day starts at 3 o'clock (adjustable).
+ - Affects day limits for all and new cards
+* Updated icons for "Word drill", "Spaced repetition" and "Search"
+* Commercial version requires activation: online or offline.
+
+Windows:
+
+* Installer removes Start menu entries of previous versions
+
+
+ 1.1.1
+==================================
+
+* Critical bug fix: can't create new dictionary
+
+
+ 1.1.0
+==================================
+
+* Modifying records in the dictionary updates the shown cards at the study window
+* Save last used working directory in the user settings.
+* Can undo the following actions in the dictionary view:
+ - modify a record
+ - insert/paste records
+ - remove/cut records
+* Modern search pane in the dictionary view
+* Can edit the current card at the study view
+ - The editing view has the same context menu as the dictionary view: remove, insert, copy, paste, cut records.
+* Can delete the current card at the study view
+* The dictionary files (.fmd) are associated with Fresh Memory application. Can double-click a dictionary to open it.
+* Shrink row height and show grid in the dictionary view.
+* In "Recent files" menu: add path after file name
+
+Minor:
+
+* Changes in interface of the study window
+* "About" dialog:
+ - updated Qt icon
+ - added build date and revision
+* New study file format (1.1):
+ - save current card name
+ - save intervals of delayed cards
+* Change default study configuration:
+ - increase new card day limit to 20.
+* Linux deb-package:
+ - file association with .fmd dictionaries + application icon.
+ - enlarge main icon to 128x128.
+* Windows installer:
+ - application icon in the Start menu and Explorer
+ - file association with .fmd dictionaries + application icon
+* Project file:
+ - the default build mode is "release" for both Linux and Windows
+ - include generation of translations into installation
+ - Linux: register/unregister the .fmd file association in installing/uninstalling
+* Bug fixes:
+ - Don't use keyword highlighting inside HTML tags (e.g. <img>)
+ - Windows 7: Fix removing the Start menu links in uninstaller
+* Refactorings in the source code:
+ - Main window and application model
+ - fix relations between study models and views
+ - Copying dictionary configuration to/from dictionary options: copy study data together with packs.
+ - Two answer times: recall time and full answer time.
+
+
+
+ 1.0.2
+==================================
+* Fixed crash: With opened study window, closing the main window crashes the application.
+* Minor improvements in the study window.
+
+
+ 1.0.1
+==================================
+
+* Bug fix: The study crashed on showing answer or next card, after dictionary was changed and saved.
+* Bug fix: Can't save study data for example dictionaries.
+ - Reason: the example dictionaries are installed at a system path, not writeable by usual user.
+ - Fix: The example dictionaries are copied to the user application data directory on the first application launch.
+ - Linux: ~/.config/freshmemory/dictionaries
+ - Windows 2000/XP: C:\Documents and Settings\<User>\Application Data\freshmemory\dictionaries
+ - Windows Vista/7: C:\Users\<User>\AppData\Roaming\freshmemory\dictionaries
+ - This path is set as default for opening a dictionary.
+* Filter *.fmd files in the Open dictionary dialog.
+* Show full dictionary path in the dictionary tab tooltip.
+* Show full file paths in error dialogs.
+* Fix the "Study progress" tooltip in Spaced repetition window: unreadable yellow color on non-Ubuntu systems.
+* Translations:
+ - Added: Czech (by Pavel Fric), Finnish, Ukrainian.
+ - Improved: Russian.
+* Source project file:
+ - Fix resource paths
+ - Fix paths for Windows
+ - Search "Known issue" in README file
+
+Windows:
+* Show file paths with native path separators ('\').
+
+Linux:
+* Packaging:
+ - Debian package: added "Installed size" field (makes Ubuntu Software Center happy)
+
+
+ 1.0.0
+==================================
+* Introduced card packs:
+ - A card pack defines what fields to use at the question and answer sides
+ - Two packs are created by default in the new dictionaries: the first two fields are question and answer, and vice versa.
+ - The fields and card packs are editable
+* Because of the card packs, the dictionaries now contain records (plain data), which are automatically converted into cards according to the fields of the card pack
+* Merging of dictionary records:
+ - Records with the same question are merged into a single card. All answers are joined with ';'.
+ - Records with several questions (separated with ';') are broken down into several cards.
+* Image paths can be specified relative to the dictionary path:
+ - <img src="%/image.png"> # In the same directory as the dictionary
+ - <img src="%%/image.png"> # In <Dictionary dir>/<Dictionary file name without extension>/
+ - No need to specify absolute paths any more.
+ - It is possible to safely move dictionary with its images to another place or computer.
+* The tabs of the main view have their own close buttons. The tabs are movable.
+* Card packs with their statistics (new/scheduled) are shown in the main view.
+* Spaced repetition:
+ - Improved graphical presentation of scheduled and remained cards
+ - When all scheduled cards are reviewed, simple study statistics are shown
+* Field styles:
+ - Each record field has its style: font and color
+ - Set of predefined field styles: Normal, Example, Pronunciation, Big, Color
+ - Fonts and colors of the styles are adjustable
+* Card background color can be changed
+* Study settings
+ - Different limits for card studying
+ - Random or sequential card show
+* The study file is saved at the same directory as the dictionary. The study file has the same file name as the dictionary, and the extension is .fms.
+* New format for dictionaries and study files. Backwards compatibility with v. 0.4 and 0.3.
+* Installation packages:
+ - Windows: exe-installer
+ - Linux: deb-package
+* Translations:
+ - Russian
+
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..2c67bd9
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,36 @@
+node {
+ stage('Checkout') {
+ checkout scm
+ }
+
+ stage('Build') {
+ if(fileExists('Makefile'))
+ sh 'make distclean'
+ sh '''
+ qmake
+ make -j4
+ '''
+ }
+
+ stage('Build test') {
+ if(fileExists('tests/unit/Makefile'))
+ sh '''
+ cd tests/unit
+ make distclean
+ '''
+ sh '''
+ cd tests/unit
+ qmake
+ make -j4
+ '''
+ }
+
+ stage('Run tests') {
+ sh '''
+ cd tests/unit
+ xvfb-run ./unittest --gtest_output=xml:gtestreport.xml
+ '''
+ junit 'tests/unit/*.xml'
+ }
+}
+
diff --git a/README b/README
new file mode 100644
index 0000000..29826a8
--- /dev/null
+++ b/README
@@ -0,0 +1,244 @@
+FRESH MEMORY
+http://fresh-memory.com
+http://freshmemory.sourceforge.net
+
+
+Fresh Memory is an education application for studying languages with Spaced Repetition method and flashcards. Its primary purpose is to study and repeat vocabulary of foreign languages. But other disciplines can be studied as well: history, geography, medicine, mathematics. The study material is stored as collections of flashcards. The flashcards may have several fields, and the user controls what combination of fields to learn. The flash cards can have formatted text and images. The look of flashcards and studying parameters are can be flexibly configured.
+
+Translations: Czech, Finnish, French, German, Russian, Spanish, Ukrainian
+
+
+FEATURES
+=============================
+
+* Efficient study method: Spaced Repetition
+* Simple review of flashcards
+* Mode for entering exact answer
+* Flash cards are groupped into dictionaries, which are stored in separate files
+* Import and export dictionaries from/to CSV files
+* Shows studying statistics: studied, scheduled cards and current study progress
+* Rich formatting of cards: colors, fonts and images
+* Multi-sided flash cards: can have several fields
+* Possibility to define combinations of used fields for studying
+* Cards with the same question are automatically merged into one card
+* Support Undo for dictionary modifications
+* Editing and removing of cards directly from the study view
+* Different card fields can have different styles: font and color
+* Adjustable background color and fonts of the flash cards
+* Automatic highlight of question keywords
+* Configurable study parameters: random or sequential card show, different day limits
+* Full support of Unicode (UTF-8): any international characters can be used.
+
+
+TECHNICAL INFORMATION
+=============================
+
+* Programming language: C++
+* User interface: Qt 5
+* External data storage: XML
+* Settings format: INI
+
+Supported operating systems:
+ * Ubuntu Linux - available deb-package
+ * Windows XP/Vista/7/8/10 - available installer
+
+
+RUN-TIME REQUIREMENTS
+=============================
+
+No special requirements for memory and processor
+
+Linux:
+ * libqt4 >= 5.2
+ * glibc
+ * openssl
+
+Windows:
+(included in the installer)
+ * Qt >= 5.2
+ * MinGW
+ * openssl
+
+
+SOURCE CODE
+=============================
+
+The source code is stored in Mercurial version control system.
+
+Clone Mercurial repository (read-only):
+ hg clone http://hg.code.sf.net/p/freshmemory/code freshmemory-code
+
+Browse source code online:
+ http://sourceforge.net/p/freshmemory/code
+
+
+COMPILATION
+=============================
+
+Ubuntu Linux requirements:
+ qt5-default
+ qtbase5-dev
+ qttools5-dev-tools
+ qttranslations5-l10n
+ g++
+ libssl-dev
+ mercurial
+
+Windows requirements:
+ * Qt >= 5.2
+ * MinGW (make, gcc)
+ * TortoiseHg
+ * Set QTDIR=C:\Qt\5.2.1
+
+Compilation commands:
+ qmake
+ make
+
+The default compilation mode in is "release". To compile in debug mode, run qmake with debug configuration:
+ qmake -config debug
+ make
+
+Notes:
+ * qmake uses Mercurial to get the current revision number. It is displayed in the "About" dialog of Freshmemory.
+ If Mercurial is not installed, the compilation still succeeds, but the "About" dialog will have empty revision number.
+ * Windows: Compile in the Qt command prompt
+
+
+INSTALLATION
+=============================
+
+Requirements:
+ * "lrelease" (from Qt SDK) can be found in the PATH
+
+Run:
+ Linux:
+ sudo make install
+
+ Windows:
+ qt-win\copy-qtdlls.bat
+ make install
+
+For Windows: "qt-win\copy-qtdlls.bat" copies needed Qt and MinGW dll's to qt-win directory. The "make install" then copies them to the installation directory.
+
+Known issues:
+------------
+* Installation also generates the translation *.qm files with "lrelease". If "lrelease" cannot be found in the PATH, it is possible to manually generate the translations before running "make install":
+ lrelease freshmemory.pro
+
+* The "make install" doesn't work on Windows Vista/7.
+ There are 2 reasons:
+ - qmake can't install to paths with spaces on Win Vista/7 (e.g. "C:\Program Files). This however works normally on Win 2000/XP.
+ - the directory for system-wide application data is different on Win Vista/7 and Win 2000/XP. It is not possible to get this directory with one system variable on both platforms. This directory can be read from Windows registry, but qmake doesn't support it.
+ Workarounds:
+ 1. manually copy all needed installation files:
+ - freshmemory.exe C:\Program Files\Freshmemory
+ - qt-win\*.dll C:\Program Files\Freshmemory
+ - qt-win\imageformats\*.dll C:\Program Files\Freshmemory
+ - dictionaries C:\Program Files\Freshmemory\dictionaries
+ - tr\*.qm C:\Program Files\Freshmemory\tr
+ - config\freshmemory.ini C:\ProgramData\freshmemory\
+ 2. generate installer package (from freshmemory.nsis) and install with it
+ 3. develop on Win 2000/XP instead of Win 2000/7.
+
+
+UNINSTALLATION
+=============================
+
+Run:
+ Linux:
+ sudo make uninstall
+ Windows:
+ make uninstall
+
+Known issues:
+------------
+* Windows cannot correctly finish the uninstallation. MinGW doesn't recognize paths with spaces:
+ rm -r /cygdrive/c/Program Files/Freshmemory/dictionaries/chemical-elements.fmd
+ rm: cannot remove `/cygdrive/c/Program': No such file or directory
+ rm: cannot remove `Files/Freshmemory/dictionaries/chemical-elements.fmd': No such file or directory
+
+
+BUILDING PACKAGES
+============================
+
+Read /packaging/packaging.txt about creation of installation packages:
+ - Linux deb-package
+ - Windows installer
+
+
+LOCALIZATION
+============================
+
+Localization is implemented with usual Qt tools: Qt Linguist and .ts-files.
+
+During installation (make install), the binary .qm translations are automatically generated from .ts-files and copied to their destination:
+ - Linux: /usr/share/freshmemory/tr
+ - Windows: C:\Program Files\Freshmemory\tr
+
+Fresh Memory uses language strings defined with:
+ - Linux: LANG system variable. For example: LANG="ru_RU".
+ - Windows: current locale ("Format") set in the Control Panel.
+
+Known issues:
+------------
+Windows:
+* The correct language is not detected by the application, English text is shown instead.
+ Windows has three locale sets: keyboard, formats and display language. Qt library detects used language according to the "Format" locale, not "Display language" as it should be. For normal localized Windows systems this is not a problem - all locales are the same. The problem is visible if e.g. the display language in "English US" Windows is changed to another one.
+ Reason: some bug in the implementation of Qt library: QLocale::system(). Though, this problem can be seen with Gtk+ too (for example in Gimp).
+ Workaround: If you want to change language of Fresh Memory, change the "Format" locale (Dates, numbers) in Windows.
+
+
+FILE ASSOCIATIONS
+=============================
+
+The dictionaries (.fmd-files) are automatically registered to open with Freshmemory. They get the Freshmemory icon and description in the file properties.
+A special MIME-type is registered for the dictionaries: application/x-fm-dictionary.
+
+The file association scripts are run on installation and uninstallation.
+This is achieved with the following files depending on the platform:
+
+* Linux:
+ debian/freshmemory.desktop
+ images/freshmemory.png
+ debian/freshmemory.xml
+ debian/postinst
+ debian/prerm
+* Windows:
+ freshmemory.rc
+ images/mainicon.ico
+ FileAssociation.nsh
+ freshmemory.nsi
+
+DOCUMENTATION
+=============================
+
+User manual: fresh-memory.com/docs
+
+
+BUGS AND FEEDBACK
+=============================
+
+Bug tracker:
+ http://sourceforge.net/p/freshmemory/bugs/
+
+Feature requests:
+ http://sourceforge.net/p/freshmemory/feature-requests/
+
+Support requests:
+ http://sourceforge.net/p/freshmemory/support-requests/
+
+
+USAGE
+============================
+
+freshmemory [OPTIONS] [DICTIONARY]
+
+Argument:
+ DICTIONARY - a dictionary filename to be loaded.
+
+Options:
+ --version, -v Prints version information and quits.
+ --help, -h Prints help page and quits.
+
+On Windows, output to console is possible only in the debug version.
+
diff --git a/application.qrc b/application.qrc
new file mode 100644
index 0000000..9ed7691
--- /dev/null
+++ b/application.qrc
@@ -0,0 +1,59 @@
+<RCC>
+ <qresource prefix="/">
+ <file>images/freshmemory.png</file>
+ <file>images/filenew.png</file>
+ <file>images/fileopen.png</file>
+ <file>images/filesave.png</file>
+ <file>images/filesaveas.png</file>
+ <file>images/remove.png</file>
+ <file>images/exit.png</file>
+ <file>images/editcut.png</file>
+ <file>images/editcopy.png</file>
+ <file>images/editpaste.png</file>
+ <file>images/add.png</file>
+ <file>images/delete.png</file>
+ <file>images/find.png</file>
+ <file>images/next.png</file>
+ <file>images/flashcards-24.png</file>
+ <file>images/back.png</file>
+ <file>images/forward.png</file>
+ <file>images/up.png</file>
+ <file>images/down.png</file>
+ <file>images/font-style.png</file>
+ <file>images/info.png</file>
+ <file>images/openbook-24.png</file>
+ <file>images/1leftarrow.png</file>
+ <file>images/1rightarrow.png</file>
+ <file>images/1uparrow.png</file>
+ <file>images/1downarrow.png</file>
+ <file>images/warning.png</file>
+ <file>images/gray-cross.png</file>
+ <file>images/fields.png</file>
+ <file>images/spaced-rep.png</file>
+ <file>images/word-drill.png</file>
+ <file>images/dic-options.png</file>
+ <file>images/study-settings.png</file>
+ <file>images/passes.png</file>
+ <file>images/continue-search.png</file>
+ <file>images/Aa.png</file>
+ <file>images/RX.png</file>
+ <file>images/whole-words.png</file>
+ <file>images/selection.png</file>
+ <file>images/pencil.png</file>
+ <file>images/red-cross.png</file>
+ <file>images/broken-image.png</file>
+ <file>images/add-image.png</file>
+ <file>images/statistics.png</file>
+ <file>images/language.png</file>
+ <file>images/question.png</file>
+ <file>images/green-tick.png</file>
+ <file>images/green-triangle-up.png</file>
+ <file>images/blue-triangle-down.png</file>
+ <file>images/red-stop.png</file>
+ <file>images/new-topright.png</file>
+ <file>images/chart-past.png</file>
+ <file>images/chart-future.png</file>
+ <file>images/pie-chart-3d.png</file>
+ <file>images/gplv3-88x31.png</file>
+ </qresource>
+</RCC>
diff --git a/copy-docs.sh b/copy-docs.sh
new file mode 100755
index 0000000..1cd7dd5
--- /dev/null
+++ b/copy-docs.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+VERSION=`cut -d "." -f 1-2 version.txt`
+cd userdocs
+DEST=../../fmsite/freshmemory/static/docs/$VERSION
+mkdir $DEST
+make clean html
+rm -r _build/html/_sources 2> /dev/null
+cp -r _build/html/* $DEST
+cd ..
diff --git a/doc/chart.xcf b/doc/chart.xcf
new file mode 100644
index 0000000..47c1093
--- /dev/null
+++ b/doc/chart.xcf
Binary files differ
diff --git a/doc/config/freshmemory.ini b/doc/config/freshmemory.ini
new file mode 100644
index 0000000..0dba0eb
--- /dev/null
+++ b/doc/config/freshmemory.ini
@@ -0,0 +1,59 @@
+[Styles]
+bg-color=white
+list=Normal, Example, Transcription, Big, Color1, Color2
+
+Normal\font-family=Times New Roman
+Normal\font-size=18
+Normal\font-bold=true
+Normal\font-italic=false
+Normal\color=black
+Example\font-bold=false
+Example\font-size=14
+Example\keyword=true
+Example\keyword\color=blue
+Transcription\font-family=Arial
+Transcription\font-bold=false
+Transcription\prefix=/
+Transcription\suffix=/
+Big\font-family=Arial
+Big\font-bold=false
+Big\font-size=26
+Color1\color=red
+Color2\color=blue
+
+[Study]
+random=true
+new-cards-share=0.2
+scheduling-randomness=0.1
+cards-daylimit=70
+new-cards-daylimit=20
+pack-timelimit=15
+dayshift=3
+
+init-easiness=2.5
+min-easiness=1.3
+max-easiness=3.2
+init-interval=1
+good-grade=4
+
+dE\0=0, 0, 0, 0, 0, 0
+dE\1=0, 0, 0, 0, 0, 0
+dE\2=0, 0, -0.32, -0.14, 0, 0.1
+dE\3=0, 0, -0.32, -0.14, 0, 0.1
+dE\4=0, 0, -0.32, -0.14, 0, 0.1
+dE\5=0, 0, -0.32, -0.14, 0, 0.1
+
+interval\0=0, 0, 0.9, 0.9, 0.9, 0.9
+interval\1=0, 0, 0.9, 0.9, 0.9, 0.9
+interval\2=0, 0, -1, -1, -1, -1
+interval\3=0, 0, -1, -1, -1, -1
+interval\4=0, 0, -1, -1, -1, -1
+interval\5=0, 0, -1, -1, -1, -1
+
+cards\0=7, 15, 0, 0, 0, 0
+cards\1=7, 15, 0, 0, 0, 0
+cards\2=7, 15, 0, 0, 0, 0
+cards\3=7, 15, 0, 0, 0, 0
+cards\4=7, 15, 0, 0, 0, 0
+cards\5=7, 15, 0, 0, 0, 0
+
diff --git a/doc/config/install.txt b/doc/config/install.txt
new file mode 100644
index 0000000..15d2d1b
--- /dev/null
+++ b/doc/config/install.txt
@@ -0,0 +1,23 @@
+System-wide settings
+======================
+
+ freshmemory.ini
+
+Linux:
+ /etc/xdg/freshememory/
+
+Windows:
+ Registry: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders\Common AppData
+
+Win XP:
+ C:\Documents and Settings\All Users\Application Data\freshmemory
+ %ALLUSERSPROFILE%\Application Data
+
+Win Vista:
+Win 7:
+ C:\ProgramData\freshmemory
+ C:\Users\All Users\freshmemory
+ %ALLUSERSPROFILE%
+ %ProgramData% (New)
+
+Source article: http://www.adminxp.com/windowsvista/index.php?aid=235 \ No newline at end of file
diff --git a/doc/dict-format.txt b/doc/dict-format.txt
new file mode 100644
index 0000000..946622f
--- /dev/null
+++ b/doc/dict-format.txt
@@ -0,0 +1,147 @@
+Dictionary file example:
+(*.fmd)
+===============================================================
+
+Version 1.4
+
+* Added pack attribute "exact-ans":
+===============================================================
+ <pack exact-ans="true">
+ <qst>Russian</qst>
+ <ans>Finnish</ans>
+ <ans>Example</ans>
+ </pack>
+===============================================================
+
+
+
+Version 1.0
+
+* The default field ids for cards are taken from fields::field tags.
+* The question and answer field indices are defined as parameters of fileds::field tags.
+
+===============================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-dict>
+<dict version="1.0">
+ <fields>
+ <field > Finnish </field>
+ <field > Russian </field>
+ <field style="Example"> Example </field>
+ <field > English </field>
+ <field style="Transcription"> Transcription </field>
+ </fields>
+ <packs>
+ <pack>
+ <qst>Finnish</qst>
+ <ans>Russian</ans>
+ <ans>Example</ans>
+ </pack>
+ <pack>
+ <qst>Russian</qst>
+ <ans>Finnish</ans>
+ <ans>Example</ans>
+ </pack>
+ </packs>
+ <entries>
+ <e tags="first, vaikeat sanat">
+ <f> perehtyä </f>
+ <f> ознакомиться </f>
+ </e>
+ <e tags="first">
+ <f> uhkaus </f>
+ <f> угроза </f>
+ <f> suuri uhkaus </f>
+ </e>
+ <e>
+ <f> усваивать </f>
+ </e>
+ <e>
+ <f> harrastaa </f>
+ <f> заниматься </f>
+ </e>
+ <e tags="vaikeat sanat">
+ <f> alkuperäinen </f>
+ <f> начальный, первоначальный </f>
+ <f> luen [alkuperäisen] kirjan </f>
+ </e>
+ <e>
+ <f> lunastaa </field>
+ <f> выкупать </field>
+ </e>
+ </entries>
+</dict>
+
+
+Version 0.4
+===============================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-dict>
+<dict version="0.4" id="{550e8400-e29b-41d4-a716-446655440000}">
+ <fields>
+ <field id="{550e8400-e29b-41d4-a716-446655440001}" question="yes"> Finnish </field>
+ <field id="{550e8400-e29b-41d4-a716-446655440002}" answer="0"> Russian </field>
+ <field id="{550e8400-e29b-41d4-a716-446655440003}" answer="1" style="example"> Example </field>
+ <field id="{550e8400-e29b-41d4-a716-446655440004}"> English </field>
+ </fields>
+ <cards>
+ <c id="{770e8400-e29b-41d4-a716-446655440001}">
+ <f> perehtyä </f>
+ <f> ознакомиться </f>
+ </c>
+ <c id="{770e8400-e29b-41d4-a716-446655440002}">
+ <f> uhkaus </f>
+ <f> угроза </f>
+ <f id="{550e8400-e29b-41d4-a716-446655440003}"> suuri uhkaus </f>
+ </c>
+ <c id="{770e8400-e29b-41d4-a716-446655440003}">
+ <f> усваивать </f>
+ </c>
+ <c id="{770e8400-e29b-41d4-a716-446655440004}" comment="yes">
+ <f> harrastaa </f>
+ <f> заниматься </f>
+ </c>
+ <c id="{770e8400-e29b-41d4-a716-446655440005}">
+ <f> alkuperäinen </f>
+ <f> начальный, первоначальный </f>
+ <f> luen [alkuperäisen] kirjan </f>
+ </c>
+ <c id="{340e8422-e29b-41d4-a716-446655440123}" orig-dic="{d4fe8439-e20b-41d4-a716-2816655440173}">
+ <f orig-id="{220e8422-e29b-41d4-a716-446655440001}> lunastaa </field>
+ <f orig-id="{220e8422-e29b-41d4-a716-446655440002}"> выкупать </field>
+ </c>
+ </cards>
+</dict>
+===============================================================
+
+Version 0.3
+===============================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-dict>
+<dict id="dic-suomi-20071108@20071112030550" version="0.3">
+ <card>
+ <qst> perehtyä </qst>
+ <ans> ознакомиться </ans>
+ </card>
+ <card>
+ <qst> uhkaus </qst>
+ <ans> угроза </ans>
+ <xmp> suuri uhkaus </xmp>
+ </card>
+ <card>
+ <ans> усваивать </ans>
+ </card>
+ <card>
+ <qst> #harrastaa </qst>
+ <ans> заниматься </ans>
+ </card>
+ <card>
+ <qst> alkuperäinen </qst>
+ <ans> начальный, первоначальный </ans>
+ <xmp> luen %alkuperäisen kirjan </xmp>
+ </card>
+ <card>
+ <qst> lunastaa </qst>
+ <ans> выкупать </ans>
+ </card>
+</dict>
diff --git a/doc/file-associations.txt b/doc/file-associations.txt
new file mode 100644
index 0000000..90dffd3
--- /dev/null
+++ b/doc/file-associations.txt
@@ -0,0 +1,29 @@
+http://ubuntuforums.org/showthread.php?t=2003610
+
+
+Add MIME type
+===========================
+
+
+<?xml version='1.0' encoding='utf-8'?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+ <mime-type type="application/rails">
+ <comment>Rails</comment>
+ <glob pattern="*.rails"/>
+ </mime-type>
+</mime-info>
+
+
+sudo xdg-mime install --novendor ~/Documents/rails.xml
+
+sudo update-mime-database /usr/share/mime
+
+sudo gedit /usr/share/applications/rails.desktop
+
+In the .desktop file:
+
+ MimeType=application/rails
+
+sudo xdg-desktop-menu install --novendor /usr/share/applications/rails.desktop
+
+sudo xdg-icon-resource install --context mimetypes --size 128 freshmemory.png application-x-fm-dictionary
diff --git a/doc/pie-chart.xcf b/doc/pie-chart.xcf
new file mode 100644
index 0000000..a27b43d
--- /dev/null
+++ b/doc/pie-chart.xcf
Binary files differ
diff --git a/doc/study-format.txt b/doc/study-format.txt
new file mode 100644
index 0000000..bca1f3e
--- /dev/null
+++ b/doc/study-format.txt
@@ -0,0 +1,169 @@
+=================================================================
+Study file example
+==================================================================
+
+Version 1.4
+==========================
+
+* Removed card-based interval: <r ... c= />
+* Removed <delay /> tag (in the end)
+* Grades changed: ("2" removed)
+ - 0 -> 1
+ - 1 -> 2
+ - 2 -> 3
+* Added level param: <r l="3" ... />
+ Add to old tags using interval:
+ - i=0, c=7 -> i=<unknown>, l=2 (short learning)
+ - i=0, c=15 -> i=<incorrect>, l=2 (short learning)
+ - i>0 (also 0.9 days) -> l=10 (repeating)
+ "Learning" (10 min) interval was not used before. Level "long learning" (3) will not be added.
+
+
+
+*** dictionary.fms ****
+=================================================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-study>
+<study version="1.4">
+ <pack id="Finnish Russian Example">
+ <cur-card id="hiilivety"/>
+ <c id="kukkaro">
+ <r d="2009-10-05T17:05:16" l="10" g="4" e="2.5" rt="3.7" at="4.7" i="2.68" />
+ <r d="2009-10-06T17:05:16" l="10" g="4" e="2.5" rt="2.1" at="3.2" i="5.68" />
+ <r d="2009-10-07T17:05:16" l="10" g="4" e="2.5" rt="4.8" at="8.7" i="8.68" />
+ </c>
+ <c id="reunaviiva">
+ <r d="2009-10-05T17:05:16" l="2" g="1" e="2.18" rt="4.7" at="6.1" i="0.000157"/>
+ <r d="2009-10-05T17:07:52" l="3" g="4" e="2.4" rt="5.4" at="7.1" i="0.9" />
+ <r d="2009-10-07T17:05:16" l="10" g="4" e="2.4" rt="5.3" at="10.3" i="2.52" />
+ </c>
+ <c id="mitä parhain">
+ <r d="2009-10-06T17:05:16" l="10" g="4" e="2.5" rt="3.5" at="7.9" i="5.68" />
+ <r d="2009-10-07T17:05:16" l="2" g="2" e="2.5" rt="5.1" at="12.6" i="0.00032" />
+ </c>
+ </pack>
+</study>
+
+
+
+Version 1.2
+==========================
+
+* The card-based interval cannot be "after all" (-1).
+
+
+Version 1.1
+==========================
+
+* Saved current selected card for Spaced repetition (it's not yet in the study history):
+ - tag <cur-card> in the beginning of <pack> tag.
+* New fields:
+ * c = card-based interval
+ - If 'c' attribute is present, then 'i' (day interval) == 0.
+ - Either 'c' or 'i' (day interval) can be present at the same time.
+ * rt = recall time
+ * at = full answer time
+* Remaining card-based intervals are saved as separate last study record:
+ - <delay c="8"/>
+* If the card-based interval is after all cards, it is == -1.
+* Attribute order changed:
+ - <r d= g= e= rt= at= [ic]= />
+
+*** dictionary.fms ****
+=================================================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-study>
+<study version="1.0">
+ <pack id="Finnish Russian Example">
+ <cur-card id="hiilivety"/>
+ <c id="kukkaro">
+ <r d="2009-10-05T17:05:16" g="4" e="2.5" rt="3.7" at="4.7" i="2.68" />
+ <r d="2009-10-06T17:05:16" g="4" e="2.5" rt="2.1" at="3.2" i="5.68" />
+ <r d="2009-10-07T17:05:16" g="4" e="2.5" rt="4.8" at="8.7" i="8.68" />
+ </c>
+ <c id="reunaviiva">
+ <r d="2009-10-05T17:05:16" g="0" e="2.18" rt="4.7" at="6.1" c="7" />
+ <r d="2009-10-05T17:07:52" g="4" e="2.4" rt="5.4" at="7.1" i="0.9" />
+ <r d="2009-10-07T17:05:16" g="4" e="2.4" rt="5.3" at="10.3" i="2.52" />
+ </c>
+ <c id="mit� parhain">
+ <r d="2009-10-06T17:05:16" g="4" e="2.5" rt="3.5" at="7.9" i="5.68" />
+ <r d="2009-10-07T17:05:16" g="1" e="2.5" rt="5.1" at="12.6" c="15" />
+ <delay c="4"/>
+ </c>
+ </pack>
+</study>
+
+
+
+Version 1.0
+==========================
+
+* The study file contains the whole history of repetitions.
+* Card interval delays are not saved!!! The delayed cards will be shown first at the next session.
+* Packs and cards are matched with the dictionary file by IDs (open strings)
+ - Pack ID is space delimited list of its field names: question, main answer, other answers.
+ - Card ID is its question
+* New cards can be seen with the study history (only 1 record)
+* Study record fields:
+ d = date
+ i = next interval (days)
+ g = grade
+ e = easiness
+ t = answering time (seconds)
+
+*** dictionary.fms ****
+=================================================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-study>
+<study version="1.0">
+ <pack id="Finnish Russian Example">
+ <c id="kukkaro">
+ <r d="2009-10-05T17:05:16" i="2.68" g="4" e="2.5" t="4.7"/>
+ <r d="2009-10-06T17:05:16" i="5.68" g="4" e="2.5" t="3.2"/>
+ <r d="2009-10-07T17:05:16" i="8.68" g="4" e="2.5" t="4.8"/>
+ </c>
+ <c id="reunaviiva">
+ <r d="2009-10-05T17:05:16" i="0" g="0" e="2.18" t="6.1"/>
+ <r d="2009-10-05T17:07:52" i="1" g="4" e="2.4" t="5.4"/>
+ <r d="2009-10-07T17:05:16" i="2.52" g="4" e="2.4" t="5.3"/>
+ </c>
+ <c id="mit� parhain">
+ <r d="2009-10-06T17:05:16" i="5.68" g="4" e="2.5" t="3.5"/>
+ </c>
+ </pack>
+</study>
+
+
+Version 0.4
+==========================
+
+* Active deadline must not be saved! Shifted active deadline is forgotten after a spacedrep session is closed.
+* Status of unrepeated card can be seen after loading.
+* Postponing length (Num of cards) is saved in cards section
+* Fresh cards can be regenerated at new spacedrep start.
+* The default question-answer pair is the first two fields of the dictionary.
+* Reversed two first fields are defined by argument s:rev="yes".
+
+*** .config/freshmemory/{550e8400-e29b-41d4-a716-446655440001}.fms ****
+=================================================================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-study>
+<study version="0.4">
+ <unrep added="2008-09-23T21:47:28" used="7" />
+ <unrep qst="{550e8400-e29b-41d4-a716-446655440002}" ans="{550e8400-e29b-41d4-a716-446655440001}"
+ added="2008-09-23T21:47:28" used="15" />
+ <c id="{220e8400-e29b-41d4-a716-446655440001}" name="table">
+ <s qst="{550e8400-e29b-41d4-a716-446655440001}" ans="{550e8400-e29b-41d4-a716-446655440002}"
+ last="2008-09-23T21:47:28" grd="4" eas="2.5472" int="5.32" />
+ <s qst="{550e8400-e29b-41d4-a716-446655440002}" rev="yes"
+ last="2008-06-04T13:26:05" grd="5" eas="3.2" int="0" cards="3" />
+ </c>
+ <c id="{220e8400-e29b-41d4-a716-446655440001}" name="ball">
+ <s last="2008-09-23T21:47:28" grd="4" eas="2.5472" int="5.32" />
+ <s rev="yes" last="2008-06-04T13:26:05" grd="5" eas="3.2" int="0" cards="3" />
+ <s qst="{550e8400-e29b-41d4-a716-446655440002}" ans="{550e8400-e29b-41d4-a716-446655440001}"
+ last="2008-07-14T18:03:22" grd="4" eas="2.4" int="4.6" />
+ </c>
+</study>
+
diff --git a/examples/countries-europe-2.fmd b/examples/countries-europe-2.fmd
new file mode 100644
index 0000000..0741b66
--- /dev/null
+++ b/examples/countries-europe-2.fmd
@@ -0,0 +1,259 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE freshmemory-dict>
+<dict version="1.0">
+ <fields>
+ <field>Country</field>
+ <field>Capital</field>
+ <field>Flag</field>
+ </fields>
+ <packs>
+ <pack>
+ <qst>Country</qst>
+ <ans>Capital</ans>
+ </pack>
+ <pack>
+ <qst>Capital</qst>
+ <ans>Country</ans>
+ </pack>
+ <pack>
+ <qst>Country</qst>
+ <ans>Flag</ans>
+ </pack>
+ <pack>
+ <qst>Flag</qst>
+ <ans>Country</ans>
+ </pack>
+ </packs>
+ <entries>
+ <e>
+ <f>France</f>
+ <f>Paris</f>
+ <f>&lt;img src=&quot;%%/France.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Netherlands</f>
+ <f>Amsterdam</f>
+ <f>&lt;img src=&quot;%%/Netherlands.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Ukraine</f>
+ <f>Kyiv</f>
+ <f>&lt;img src=&quot;%%/Ukraine.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Sweden</f>
+ <f>Stockholm</f>
+ <f>&lt;img src=&quot;%%/Sweden.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Norway</f>
+ <f>Oslo</f>
+ <f>&lt;img src=&quot;%%/Norway.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Denmark</f>
+ <f>Copenhagen</f>
+ <f>&lt;img src=&quot;%%/Denmark.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Finland</f>
+ <f>Helsinki sdfa</f>
+ <f>&lt;img src=&quot;%%/Finland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Iceland</f>
+ <f>Reykjavik</f>
+ <f>&lt;img src=&quot;%%/Iceland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Great Britain</f>
+ <f>London</f>
+ <f>&lt;img src=&quot;%%/Great Britain.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Spain</f>
+ <f>Madrid</f>
+ <f>Computer &lt;img src=&quot;%%/Spain.png&quot;&gt; Monitor hello &lt;img src=&quot;%%/Croatia.png&quot;&gt; After text</f>
+ </e>
+ <e>
+ <f>Italy</f>
+ <f>Rome</f>
+ <f>Hello computer &lt;img src=&quot;%%/Italy.png&quot;&gt; Some text here</f>
+ </e>
+ <e>
+ <f>Germany</f>
+ <f>Berlin</f>
+ <f>&lt;img src=&quot;%%/Germany.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Poland</f>
+ <f>Warsaw</f>
+ <f>&lt;img src=&quot;%%/Poland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Andorra</f>
+ <f>Andorra la Vella</f>
+ <f>&lt;img src=&quot;%%/Andorra.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Greece</f>
+ <f>Athens</f>
+ <f>&lt;img src=&quot;%%/Greece.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Serbia</f>
+ <f>Belgrade</f>
+ <f>&lt;img src=&quot;%%/Serbia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Russia</f>
+ <f>Moscow</f>
+ <f>&lt;img src=&quot;%%/Russia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Germany</f>
+ <f>Berlin</f>
+ <f>&lt;img src=&quot;%%/Germany.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Switzerland</f>
+ <f>Bern</f>
+ <f>&lt;img src=&quot;%%/Switzerland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Slovakia</f>
+ <f>Bratislava</f>
+ <f>&lt;img src=&quot;%%/Slovakia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Belgium</f>
+ <f>Brussels</f>
+ <f>&lt;img src=&quot;%%/Belgium.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Romania</f>
+ <f>Bucharest</f>
+ <f>&lt;img src=&quot;%%/Romania.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Hungary</f>
+ <f>Budapest</f>
+ <f>&lt;img src=&quot;%%/Hungary.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Moldova</f>
+ <f>Chisinau</f>
+ <f>&lt;img src=&quot;%%/Moldova.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Ireland</f>
+ <f>Dublin</f>
+ <f>&lt;img src=&quot;%%/Ireland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Portugal</f>
+ <f>Lisbon</f>
+ <f>&lt;img src=&quot;%%/Portugal.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Slovenia</f>
+ <f>Ljubljana</f>
+ <f>&lt;img src=&quot;%%/Slovenia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Luxembourg</f>
+ <f>Luxembourg</f>
+ <f>&lt;img src=&quot;%%/Luxembourg.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Belarus</f>
+ <f>Minsk</f>
+ <f>&lt;img src=&quot;%%/Belarus.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Monaco</f>
+ <f>Monaco</f>
+ <f>&lt;img src=&quot;%%/Monaco.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Cyprus</f>
+ <f>Nicosia</f>
+ <f>&lt;img src=&quot;%%/Cyprus.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Greenland</f>
+ <f>Nuuk</f>
+ <f>&lt;img src=&quot;%%/Greenland.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Montenegro</f>
+ <f>Podgorica</f>
+ <f>&lt;img src=&quot;%%/Montenegro.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Czech Republic</f>
+ <f>Prague</f>
+ <f>&lt;img src=&quot;%%/Czech Republic.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Latvia</f>
+ <f>Riga</f>
+ <f>&lt;img src=&quot;%%/Latvia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>San Marino</f>
+ <f>San Marino</f>
+ <f>&lt;img src=&quot;%%/San Marino.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Bosnia &amp; Herzegovina</f>
+ <f>Sarajevo</f>
+ <f>&lt;img src=&quot;%%/Bosnia &amp; Herzegovina.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Macedonia</f>
+ <f>Skopje</f>
+ <f>&lt;img src=&quot;%%/Macedonia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Bulgaria</f>
+ <f>Sofia</f>
+ <f>&lt;img src=&quot;%%/Bulgaria.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Estonia</f>
+ <f>Tallinn</f>
+ <f>&lt;img src=&quot;%%/Estonia.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Albania</f>
+ <f>Tirana</f>
+ <f>&lt;img src=&quot;%%/Albania.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Liechtenstein</f>
+ <f>Vaduz</f>
+ <f>&lt;img src=&quot;%%/Liechtenstein.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Malta</f>
+ <f>Valletta</f>
+ <f>&lt;img src=&quot;%%/Malta.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Austria</f>
+ <f>Vienna</f>
+ <f>&lt;img src=&quot;%%/Austria.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Lithuania</f>
+ <f>Vilnius</f>
+ <f>&lt;img src=&quot;%%/Lithuania.png&quot;&gt;</f>
+ </e>
+ <e>
+ <f>Croatia</f>
+ <f>Zagreb</f>
+ <f>&lt;img src=&quot;%%/Croatia.png&quot;&gt;</f>
+ </e>
+ </entries>
+</dict>
diff --git a/examples/countries-europe-2/Albania.png b/examples/countries-europe-2/Albania.png
new file mode 100644
index 0000000..66a4423
--- /dev/null
+++ b/examples/countries-europe-2/Albania.png
Binary files differ
diff --git a/examples/countries-europe-2/Andorra.png b/examples/countries-europe-2/Andorra.png
new file mode 100644
index 0000000..eb1bec6
--- /dev/null
+++ b/examples/countries-europe-2/Andorra.png
Binary files differ
diff --git a/examples/countries-europe-2/Austria.png b/examples/countries-europe-2/Austria.png
new file mode 100644
index 0000000..ef2f724
--- /dev/null
+++ b/examples/countries-europe-2/Austria.png
Binary files differ
diff --git a/examples/countries-europe-2/Belarus.png b/examples/countries-europe-2/Belarus.png
new file mode 100644
index 0000000..c68042c
--- /dev/null
+++ b/examples/countries-europe-2/Belarus.png
Binary files differ
diff --git a/examples/countries-europe-2/Belgium.png b/examples/countries-europe-2/Belgium.png
new file mode 100644
index 0000000..008b412
--- /dev/null
+++ b/examples/countries-europe-2/Belgium.png
Binary files differ
diff --git a/examples/countries-europe-2/Bosnia & Herzegovina.png b/examples/countries-europe-2/Bosnia & Herzegovina.png
new file mode 100644
index 0000000..59045df
--- /dev/null
+++ b/examples/countries-europe-2/Bosnia & Herzegovina.png
Binary files differ
diff --git a/examples/countries-europe-2/Bulgaria.png b/examples/countries-europe-2/Bulgaria.png
new file mode 100644
index 0000000..832922f
--- /dev/null
+++ b/examples/countries-europe-2/Bulgaria.png
Binary files differ
diff --git a/examples/countries-europe-2/Croatia.png b/examples/countries-europe-2/Croatia.png
new file mode 100644
index 0000000..0ba5cdc
--- /dev/null
+++ b/examples/countries-europe-2/Croatia.png
Binary files differ
diff --git a/examples/countries-europe-2/Cyprus.png b/examples/countries-europe-2/Cyprus.png
new file mode 100644
index 0000000..7c0059a
--- /dev/null
+++ b/examples/countries-europe-2/Cyprus.png
Binary files differ
diff --git a/examples/countries-europe-2/Czech Republic.png b/examples/countries-europe-2/Czech Republic.png
new file mode 100644
index 0000000..2c6cb2e
--- /dev/null
+++ b/examples/countries-europe-2/Czech Republic.png
Binary files differ
diff --git a/examples/countries-europe-2/Denmark.png b/examples/countries-europe-2/Denmark.png
new file mode 100644
index 0000000..c91547a
--- /dev/null
+++ b/examples/countries-europe-2/Denmark.png
Binary files differ
diff --git a/examples/countries-europe-2/Estonia.png b/examples/countries-europe-2/Estonia.png
new file mode 100644
index 0000000..71f56eb
--- /dev/null
+++ b/examples/countries-europe-2/Estonia.png
Binary files differ
diff --git a/examples/countries-europe-2/Finland.png b/examples/countries-europe-2/Finland.png
new file mode 100644
index 0000000..38e71c3
--- /dev/null
+++ b/examples/countries-europe-2/Finland.png
Binary files differ
diff --git a/examples/countries-europe-2/France.png b/examples/countries-europe-2/France.png
new file mode 100644
index 0000000..9b1b3a7
--- /dev/null
+++ b/examples/countries-europe-2/France.png
Binary files differ
diff --git a/examples/countries-europe-2/Germany.png b/examples/countries-europe-2/Germany.png
new file mode 100644
index 0000000..f6df975
--- /dev/null
+++ b/examples/countries-europe-2/Germany.png
Binary files differ
diff --git a/examples/countries-europe-2/Great Britain.png b/examples/countries-europe-2/Great Britain.png
new file mode 100644
index 0000000..aa61409
--- /dev/null
+++ b/examples/countries-europe-2/Great Britain.png
Binary files differ
diff --git a/examples/countries-europe-2/Greece.png b/examples/countries-europe-2/Greece.png
new file mode 100644
index 0000000..2df5cc5
--- /dev/null
+++ b/examples/countries-europe-2/Greece.png
Binary files differ
diff --git a/examples/countries-europe-2/Greenland.png b/examples/countries-europe-2/Greenland.png
new file mode 100644
index 0000000..5bff259
--- /dev/null
+++ b/examples/countries-europe-2/Greenland.png
Binary files differ
diff --git a/examples/countries-europe-2/Hungary.png b/examples/countries-europe-2/Hungary.png
new file mode 100644
index 0000000..09ec237
--- /dev/null
+++ b/examples/countries-europe-2/Hungary.png
Binary files differ
diff --git a/examples/countries-europe-2/Iceland.png b/examples/countries-europe-2/Iceland.png
new file mode 100644
index 0000000..811436e
--- /dev/null
+++ b/examples/countries-europe-2/Iceland.png
Binary files differ
diff --git a/examples/countries-europe-2/Ireland.png b/examples/countries-europe-2/Ireland.png
new file mode 100644
index 0000000..a578e43
--- /dev/null
+++ b/examples/countries-europe-2/Ireland.png
Binary files differ
diff --git a/examples/countries-europe-2/Italy.png b/examples/countries-europe-2/Italy.png
new file mode 100644
index 0000000..8d2c599
--- /dev/null
+++ b/examples/countries-europe-2/Italy.png
Binary files differ
diff --git a/examples/countries-europe-2/Latvia.png b/examples/countries-europe-2/Latvia.png
new file mode 100644
index 0000000..6ff76d2
--- /dev/null
+++ b/examples/countries-europe-2/Latvia.png
Binary files differ
diff --git a/examples/countries-europe-2/Liechtenstein.png b/examples/countries-europe-2/Liechtenstein.png
new file mode 100644
index 0000000..2e5c17a
--- /dev/null
+++ b/examples/countries-europe-2/Liechtenstein.png
Binary files differ
diff --git a/examples/countries-europe-2/Lithuania.png b/examples/countries-europe-2/Lithuania.png
new file mode 100644
index 0000000..17b9018
--- /dev/null
+++ b/examples/countries-europe-2/Lithuania.png
Binary files differ
diff --git a/examples/countries-europe-2/Luxembourg.png b/examples/countries-europe-2/Luxembourg.png
new file mode 100644
index 0000000..fa6c564
--- /dev/null
+++ b/examples/countries-europe-2/Luxembourg.png
Binary files differ
diff --git a/examples/countries-europe-2/Macedonia.png b/examples/countries-europe-2/Macedonia.png
new file mode 100644
index 0000000..91e100a
--- /dev/null
+++ b/examples/countries-europe-2/Macedonia.png
Binary files differ
diff --git a/examples/countries-europe-2/Malta.png b/examples/countries-europe-2/Malta.png
new file mode 100644
index 0000000..822af5d
--- /dev/null
+++ b/examples/countries-europe-2/Malta.png
Binary files differ
diff --git a/examples/countries-europe-2/Moldova.png b/examples/countries-europe-2/Moldova.png
new file mode 100644
index 0000000..38c53c6
--- /dev/null
+++ b/examples/countries-europe-2/Moldova.png
Binary files differ
diff --git a/examples/countries-europe-2/Monaco.png b/examples/countries-europe-2/Monaco.png
new file mode 100644
index 0000000..98e7147
--- /dev/null
+++ b/examples/countries-europe-2/Monaco.png
Binary files differ
diff --git a/examples/countries-europe-2/Montenegro.png b/examples/countries-europe-2/Montenegro.png
new file mode 100644
index 0000000..e0cdc4a
--- /dev/null
+++ b/examples/countries-europe-2/Montenegro.png
Binary files differ
diff --git a/examples/countries-europe-2/Netherlands.png b/examples/countries-europe-2/Netherlands.png
new file mode 100644
index 0000000..e6af8e6
--- /dev/null
+++ b/examples/countries-europe-2/Netherlands.png
Binary files differ
diff --git a/examples/countries-europe-2/Norway.png b/examples/countries-europe-2/Norway.png
new file mode 100644
index 0000000..6bbba1b
--- /dev/null
+++ b/examples/countries-europe-2/Norway.png
Binary files differ
diff --git a/examples/countries-europe-2/Poland.png b/examples/countries-europe-2/Poland.png
new file mode 100644
index 0000000..b3e7cce
--- /dev/null
+++ b/examples/countries-europe-2/Poland.png
Binary files differ
diff --git a/examples/countries-europe-2/Portugal.png b/examples/countries-europe-2/Portugal.png
new file mode 100644
index 0000000..b154563
--- /dev/null
+++ b/examples/countries-europe-2/Portugal.png
Binary files differ
diff --git a/examples/countries-europe-2/Romania.png b/examples/countries-europe-2/Romania.png
new file mode 100644
index 0000000..d12519e
--- /dev/null
+++ b/examples/countries-europe-2/Romania.png
Binary files differ
diff --git a/examples/countries-europe-2/Russia.png b/examples/countries-europe-2/Russia.png
new file mode 100644
index 0000000..b751c25
--- /dev/null
+++ b/examples/countries-europe-2/Russia.png
Binary files differ
diff --git a/examples/countries-europe-2/San Marino.png b/examples/countries-europe-2/San Marino.png
new file mode 100644
index 0000000..c7a683d
--- /dev/null
+++ b/examples/countries-europe-2/San Marino.png
Binary files differ
diff --git a/examples/countries-europe-2/Serbia.png b/examples/countries-europe-2/Serbia.png
new file mode 100644
index 0000000..ce93e2e
--- /dev/null
+++ b/examples/countries-europe-2/Serbia.png
Binary files differ
diff --git a/examples/countries-europe-2/Slovakia.png b/examples/countries-europe-2/Slovakia.png
new file mode 100644
index 0000000..50691b8
--- /dev/null
+++ b/examples/countries-europe-2/Slovakia.png
Binary files differ
diff --git a/examples/countries-europe-2/Slovenia.png b/examples/countries-europe-2/Slovenia.png
new file mode 100644
index 0000000..300f313
--- /dev/null
+++ b/examples/countries-europe-2/Slovenia.png
Binary files differ
diff --git a/examples/countries-europe-2/Spain.png b/examples/countries-europe-2/Spain.png
new file mode 100644
index 0000000..c53d5ea
--- /dev/null
+++ b/examples/countries-europe-2/Spain.png
Binary files differ
diff --git a/examples/countries-europe-2/Sweden.png b/examples/countries-europe-2/Sweden.png
new file mode 100644
index 0000000..61bf792
--- /dev/null
+++ b/examples/countries-europe-2/Sweden.png
Binary files differ
diff --git a/examples/countries-europe-2/Switzerland.png b/examples/countries-europe-2/Switzerland.png
new file mode 100644
index 0000000..98b0ece
--- /dev/null
+++ b/examples/countries-europe-2/Switzerland.png
Binary files differ
diff --git a/examples/countries-europe-2/Ukraine.png b/examples/countries-europe-2/Ukraine.png
new file mode 100644
index 0000000..8c95088
--- /dev/null
+++ b/examples/countries-europe-2/Ukraine.png
Binary files differ
diff --git a/freshmemory.pro b/freshmemory.pro
new file mode 100644
index 0000000..872af64
--- /dev/null
+++ b/freshmemory.pro
@@ -0,0 +1,262 @@
+TEMPLATE = app
+QMAKE_CXXFLAGS += -std=c++11
+QT += widgets
+
+HEADERS += \
+ src/main.h \
+ src/version.h \
+ src/strings.h \
+ src/utils/RandomGenerator.h \
+ src/utils/IRandomGenerator.h \
+ src/utils/TimeProvider.h \
+ src/dictionary/Card.h \
+ src/dictionary/IDictionary.h \
+ src/dictionary/Dictionary.h \
+ src/dictionary/DicRecord.h \
+ src/dictionary/Field.h \
+ src/dictionary/CardPack.h \
+ src/dictionary/ICardPack.h \
+ src/dictionary/DictionaryReader.h \
+ src/dictionary/DictionaryWriter.h \
+ src/dictionary/DicCsvWriter.h \
+ src/dictionary/TreeItem.h \
+ src/main-view/AppModel.h \
+ src/main-view/DictTableModel.h \
+ src/main-view/DictTableView.h \
+ src/main-view/DictTableDelegate.h \
+ src/main-view/DictTableDelegatePainter.h \
+ src/main-view/RecordEditor.h \
+ src/main-view/FieldContentCodec.h \
+ src/main-view/FieldContentPainter.h \
+ src/main-view/MainWindow.h \
+ src/main-view/DictionaryTabWidget.h \
+ src/main-view/FindPanel.h \
+ src/main-view/PacksTreeModel.h \
+ src/main-view/AboutDialog.h \
+ src/main-view/RecentFilesManager.h \
+ src/main-view/LanguageMenu.h \
+ src/main-view/CardPreview.h \
+ src/main-view/WelcomeScreen.h \
+ src/export-import/CsvDialog.h \
+ src/export-import/CsvImportDialog.h \
+ src/export-import/CsvExportDialog.h \
+ src/export-import/CsvData.h \
+ src/study/CardSideView.h \
+ src/study/IStudyModel.h \
+ src/study/IStudyWindow.h \
+ src/study/WordDrillModel.h \
+ src/study/WordDrillWindow.h \
+ src/study/SpacedRepetitionModel.h \
+ src/study/SpacedRepetitionWindow.h \
+ src/study/StudyRecord.h \
+ src/study/StudyFileWriter.h \
+ src/study/StudyFileReader.h \
+ src/study/StudySettings.h \
+ src/study/CardsStatusBar.h \
+ src/study/NumberFrame.h \
+ src/study/WarningPanel.h \
+ src/dic-options/DictionaryOptionsDialog.h \
+ src/dic-options/FieldsPage.h \
+ src/dic-options/FieldsListModel.h \
+ src/dic-options/PacksPage.h \
+ src/dic-options/PacksListModel.h \
+ src/dic-options/PackFieldsListModel.h \
+ src/dic-options/FieldsView.h \
+ src/dic-options/FieldStyleDelegate.h \
+ src/dic-options/FieldsPreviewModel.h \
+ src/dic-options/UnusedFieldsListModel.h \
+ src/dic-options/PackFieldsView.h \
+ src/dic-options/DraggableListModel.h \
+ src/field-styles/FieldStyle.h \
+ src/field-styles/FieldStyleFactory.h \
+ src/settings/FontColorSettingsDialog.h \
+ src/settings/ColorBox.h \
+ src/settings/StudySettingsDialog.h \
+ src/settings/StylesListModel.h \
+ src/settings/StylePreviewModel.h \
+ src/dictionary/DicCsvReader.h \
+ src/main-view/CardFilterModel.h \
+ src/main-view/UndoCommands.h \
+ src/study/CardEditDialog.h \
+ src/statistics/StatisticsView.h \
+ src/statistics/BaseStatPage.h \
+ src/statistics/TimeChartPage.h \
+ src/statistics/StudiedPage.h \
+ src/statistics/ScheduledPage.h \
+ src/statistics/ProgressPage.h \
+ src/charts/Chart.h \
+ src/charts/TimeChart.h \
+ src/charts/DataPoint.h \
+ src/charts/ChartScene.h \
+ src/charts/ChartView.h \
+ src/charts/ChartAxes.h \
+ src/charts/ChartDataLine.h \
+ src/charts/ChartMarker.h \
+ src/charts/ChartToolTip.h \
+ src/charts/PieChart.h \
+ src/charts/PieChartScene.h \
+ src/charts/PieRound.h \
+ src/charts/PieLegend.h
+
+SOURCES += \
+ src/main.cpp \
+ src/version.cpp \
+ src/strings.cpp \
+ src/utils/RandomGenerator.cpp \
+ src/utils/TimeProvider.cpp \
+ src/dictionary/Card.cpp \
+ src/dictionary/Dictionary.cpp \
+ src/dictionary/DicRecord.cpp \
+ src/dictionary/Field.cpp \
+ src/dictionary/CardPack.cpp \
+ src/dictionary/ICardPack.cpp \
+ src/dictionary/IDictionary.cpp \
+ src/dictionary/DictionaryReader.cpp \
+ src/dictionary/DictionaryWriter.cpp \
+ src/dictionary/DicCsvWriter.cpp \
+ src/main-view/AppModel.cpp \
+ src/main-view/DictTableModel.cpp \
+ src/main-view/DictTableView.cpp \
+ src/main-view/DictTableDelegate.cpp \
+ src/main-view/DictTableDelegatePainter.cpp \
+ src/main-view/RecordEditor.cpp \
+ src/main-view/FieldContentCodec.cpp \
+ src/main-view/MainWindow.cpp \
+ src/main-view/DictionaryTabWidget.cpp \
+ src/main-view/FindPanel.cpp \
+ src/main-view/PacksTreeModel.cpp \
+ src/main-view/AboutDialog.cpp \
+ src/main-view/RecentFilesManager.cpp \
+ src/main-view/LanguageMenu.cpp \
+ src/main-view/CardPreview.cpp \
+ src/main-view/WelcomeScreen.cpp \
+ src/export-import/CsvDialog.cpp \
+ src/export-import/CsvImportDialog.cpp \
+ src/export-import/CsvExportDialog.cpp \
+ src/study/IStudyModel.cpp \
+ src/study/CardSideView.cpp \
+ src/study/WordDrillModel.cpp \
+ src/study/WordDrillWindow.cpp \
+ src/study/IStudyWindow.cpp \
+ src/study/SpacedRepetitionModel.cpp \
+ src/study/SpacedRepetitionWindow.cpp \
+ src/study/StudyRecord.cpp \
+ src/study/StudyFileWriter.cpp \
+ src/study/StudyFileReader.cpp \
+ src/study/StudySettings.cpp \
+ src/study/CardsStatusBar.cpp \
+ src/study/NumberFrame.cpp \
+ src/study/WarningPanel.cpp \
+ src/dic-options/DictionaryOptionsDialog.cpp \
+ src/dic-options/FieldsPage.cpp \
+ src/dic-options/FieldsListModel.cpp \
+ src/dic-options/PacksPage.cpp \
+ src/dic-options/PacksListModel.cpp \
+ src/dic-options/PackFieldsListModel.cpp \
+ src/dic-options/FieldsView.cpp \
+ src/dic-options/FieldStyleDelegate.cpp \
+ src/dic-options/FieldsPreviewModel.cpp \
+ src/dic-options/UnusedFieldsListModel.cpp \
+ src/dic-options/PackFieldsView.cpp \
+ src/dic-options/DraggableListModel.cpp \
+ src/field-styles/FieldStyle.cpp \
+ src/field-styles/FieldStyleFactory.cpp \
+ src/settings/FontColorSettingsDialog.cpp \
+ src/settings/ColorBox.cpp \
+ src/settings/StudySettingsDialog.cpp \
+ src/settings/StylePreviewModel.cpp \
+ src/dictionary/DicCsvReader.cpp \
+ src/main-view/CardFilterModel.cpp \
+ src/main-view/UndoCommands.cpp \
+ src/study/CardEditDialog.cpp \
+ src/statistics/StatisticsView.cpp \
+ src/statistics/BaseStatPage.cpp \
+ src/statistics/TimeChartPage.cpp \
+ src/statistics/StudiedPage.cpp \
+ src/statistics/ScheduledPage.cpp \
+ src/statistics/ProgressPage.cpp \
+ src/charts/Chart.cpp \
+ src/charts/TimeChart.cpp \
+ src/charts/ChartScene.cpp \
+ src/charts/ChartView.cpp \
+ src/charts/ChartAxes.cpp \
+ src/charts/ChartDataLine.cpp \
+ src/charts/ChartMarker.cpp \
+ src/charts/ChartToolTip.cpp \
+ src/charts/PieChart.cpp \
+ src/charts/PieChartScene.cpp \
+ src/charts/PieRound.cpp \
+ src/charts/PieLegend.cpp
+
+TRANSLATIONS = \
+ tr/freshmemory_cs.ts \
+ tr/freshmemory_de.ts \
+ tr/freshmemory_en.ts \
+ tr/freshmemory_es.ts \
+ tr/freshmemory_fi.ts \
+ tr/freshmemory_fr.ts \
+ tr/freshmemory_ru.ts \
+ tr/freshmemory_uk.ts
+
+RESOURCES += application.qrc
+RC_FILE = freshmemory.rc
+TARGET = freshmemory
+
+REV_COMMAND = git rev-parse --short=12 HEAD
+unix: REV_COMMAND = $${REV_COMMAND} 2> /dev/null
+REV = $$system($${REV_COMMAND})
+REVSTR = '\\"$${REV}\\"'
+DEFINES += BUILD_REVISION=\"$${REVSTR}\"
+
+VERSIONSTR = '\\"$$cat(version.txt)\\"'
+DEFINES += FM_VERSION=\"$${VERSIONSTR}\"
+
+MOC_DIR = moc
+OBJECTS_DIR = obj
+
+TARGET = freshmemory
+CONFIG(debug, debug|release) {
+ CONFIG += console
+}
+else {
+ DEFINES += QT_NO_DEBUG_OUTPUT
+}
+
+DESTDIR = ./
+
+INSTALLS = target translations
+
+translations.extra = lrelease freshmemory.pro
+translations.files = tr/*.qm
+translations.CONFIG = no_check_exist
+
+unix: {
+ INSTALLS += desktop icon mime reg_mime
+
+ target.path = /usr/bin
+ translations.path = /usr/share/freshmemory/tr
+ desktop.files = debian/freshmemory.desktop
+ desktop.path = /usr/share/applications
+ icon.files = images/freshmemory.png
+ icon.path = /usr/share/pixmaps
+ mime.files = debian/freshmemory.xml
+ mime.uninstall = debian/prerm
+ mime.path = /usr/share/freshmemory
+ reg_mime.depends = install_desktop install_icon install_mime
+ reg_mime.path = /usr/share/freshmemory
+ reg_mime.extra = debian/postinst
+ }
+else:win32 {
+ INSTALLS += qtdlls qtplugins
+ INSTDIR = /cygdrive/c/Program Files/Freshmemory
+
+ target.path = "$$INSTDIR"
+ translations.path = "$$INSTDIR/tr"
+ qtdlls.files = qt-win/*.dll
+ qtdlls.CONFIG = no_check_exist
+ qtdlls.path = "$$INSTDIR"
+ qtplugins.files = qt-win/imageformats/*.dll
+ qtplugins.CONFIG = no_check_exist
+ qtplugins.path = "$$INSTDIR/imageformats"
+ }
+
diff --git a/freshmemory.rc b/freshmemory.rc
new file mode 100644
index 0000000..049e1f6
--- /dev/null
+++ b/freshmemory.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON DISCARDABLE "images\mainicon.ico" \ No newline at end of file
diff --git a/images/1downarrow.png b/images/1downarrow.png
new file mode 100644
index 0000000..ea9c00c
--- /dev/null
+++ b/images/1downarrow.png
Binary files differ
diff --git a/images/1leftarrow.png b/images/1leftarrow.png
new file mode 100644
index 0000000..a6c4668
--- /dev/null
+++ b/images/1leftarrow.png
Binary files differ
diff --git a/images/1rightarrow.png b/images/1rightarrow.png
new file mode 100644
index 0000000..8356b71
--- /dev/null
+++ b/images/1rightarrow.png
Binary files differ
diff --git a/images/1uparrow.png b/images/1uparrow.png
new file mode 100644
index 0000000..d6c2b99
--- /dev/null
+++ b/images/1uparrow.png
Binary files differ
diff --git a/images/Aa.png b/images/Aa.png
new file mode 100644
index 0000000..a37b74a
--- /dev/null
+++ b/images/Aa.png
Binary files differ
diff --git a/images/RX.png b/images/RX.png
new file mode 100644
index 0000000..e53b2a9
--- /dev/null
+++ b/images/RX.png
Binary files differ
diff --git a/images/add-image.png b/images/add-image.png
new file mode 100644
index 0000000..adcd7b2
--- /dev/null
+++ b/images/add-image.png
Binary files differ
diff --git a/images/add.png b/images/add.png
new file mode 100644
index 0000000..0540a9b
--- /dev/null
+++ b/images/add.png
Binary files differ
diff --git a/images/attic/clock.png b/images/attic/clock.png
new file mode 100644
index 0000000..a4794d8
--- /dev/null
+++ b/images/attic/clock.png
Binary files differ
diff --git a/images/attic/large-arrow-left.png b/images/attic/large-arrow-left.png
new file mode 100644
index 0000000..fd13531
--- /dev/null
+++ b/images/attic/large-arrow-left.png
Binary files differ
diff --git a/images/attic/large-arrow-right.png b/images/attic/large-arrow-right.png
new file mode 100644
index 0000000..4072bbd
--- /dev/null
+++ b/images/attic/large-arrow-right.png
Binary files differ
diff --git a/images/back.png b/images/back.png
new file mode 100644
index 0000000..60caccf
--- /dev/null
+++ b/images/back.png
Binary files differ
diff --git a/images/blue-triangle-down.png b/images/blue-triangle-down.png
new file mode 100644
index 0000000..bb48b1f
--- /dev/null
+++ b/images/blue-triangle-down.png
Binary files differ
diff --git a/images/broken-image.png b/images/broken-image.png
new file mode 100644
index 0000000..2b098c3
--- /dev/null
+++ b/images/broken-image.png
Binary files differ
diff --git a/images/chart-future.png b/images/chart-future.png
new file mode 100644
index 0000000..05c1e15
--- /dev/null
+++ b/images/chart-future.png
Binary files differ
diff --git a/images/chart-past.png b/images/chart-past.png
new file mode 100644
index 0000000..7575035
--- /dev/null
+++ b/images/chart-past.png
Binary files differ
diff --git a/images/continue-search.png b/images/continue-search.png
new file mode 100644
index 0000000..a5a5e86
--- /dev/null
+++ b/images/continue-search.png
Binary files differ
diff --git a/images/delete.png b/images/delete.png
new file mode 100644
index 0000000..64089d7
--- /dev/null
+++ b/images/delete.png
Binary files differ
diff --git a/images/dic-options.png b/images/dic-options.png
new file mode 100644
index 0000000..8ae34d0
--- /dev/null
+++ b/images/dic-options.png
Binary files differ
diff --git a/images/down.png b/images/down.png
new file mode 100644
index 0000000..cd92e2e
--- /dev/null
+++ b/images/down.png
Binary files differ
diff --git a/images/editcopy.png b/images/editcopy.png
new file mode 100644
index 0000000..f882aa2
--- /dev/null
+++ b/images/editcopy.png
Binary files differ
diff --git a/images/editcut.png b/images/editcut.png
new file mode 100644
index 0000000..79d2dca
--- /dev/null
+++ b/images/editcut.png
Binary files differ
diff --git a/images/editpaste.png b/images/editpaste.png
new file mode 100644
index 0000000..a192060
--- /dev/null
+++ b/images/editpaste.png
Binary files differ
diff --git a/images/exit.png b/images/exit.png
new file mode 100644
index 0000000..7445887
--- /dev/null
+++ b/images/exit.png
Binary files differ
diff --git a/images/fields.png b/images/fields.png
new file mode 100644
index 0000000..a0d02f5
--- /dev/null
+++ b/images/fields.png
Binary files differ
diff --git a/images/filenew.png b/images/filenew.png
new file mode 100644
index 0000000..6e838b3
--- /dev/null
+++ b/images/filenew.png
Binary files differ
diff --git a/images/fileopen.png b/images/fileopen.png
new file mode 100644
index 0000000..503a004
--- /dev/null
+++ b/images/fileopen.png
Binary files differ
diff --git a/images/filesave.png b/images/filesave.png
new file mode 100644
index 0000000..dd00abd
--- /dev/null
+++ b/images/filesave.png
Binary files differ
diff --git a/images/filesaveas.png b/images/filesaveas.png
new file mode 100644
index 0000000..61a080e
--- /dev/null
+++ b/images/filesaveas.png
Binary files differ
diff --git a/images/find.png b/images/find.png
new file mode 100644
index 0000000..1933c2c
--- /dev/null
+++ b/images/find.png
Binary files differ
diff --git a/images/flashcards-24.png b/images/flashcards-24.png
new file mode 100644
index 0000000..ca6fd98
--- /dev/null
+++ b/images/flashcards-24.png
Binary files differ
diff --git a/images/font-style.png b/images/font-style.png
new file mode 100644
index 0000000..6608631
--- /dev/null
+++ b/images/font-style.png
Binary files differ
diff --git a/images/forward.png b/images/forward.png
new file mode 100644
index 0000000..d3e6d1e
--- /dev/null
+++ b/images/forward.png
Binary files differ
diff --git a/images/freshmemory.png b/images/freshmemory.png
new file mode 100644
index 0000000..e2f607a
--- /dev/null
+++ b/images/freshmemory.png
Binary files differ
diff --git a/images/freshmemory.svg b/images/freshmemory.svg
new file mode 100644
index 0000000..d195c2f
--- /dev/null
+++ b/images/freshmemory.svg
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.1 "
+ width="48"
+ height="48"
+ sodipodi:docname="books2-flags-48.png">
+ <metadata
+ id="metadata8">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs6" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="640"
+ inkscape:window-height="480"
+ id="namedview4"
+ showgrid="false"
+ inkscape:zoom="5.4791667"
+ inkscape:cx="24"
+ inkscape:cy="24"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg2" />
+ <image
+ width="48"
+ height="48"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABHNCSVQICAgIfAhkiAAAFxdJREFU
+aIG1mXecVeW5/b/vLqeXOdPPVKbADDCAIE1FVMReE6PGRCFeTTXeGGP0qimmkmKMPRJzfwpXk4ti
+gthiTFApgshQZwZmhumFKWdmTq977/f+QTAkJveXxNz1z97nc3ZZaz3rffb+PBv+xXh8buP7+w/P
+mtn0yNymad8rn+06+RgpJQR/oiy9ZK3y91zzwPnnOZqfemaalPID/2kflvDJeHxuI184eASAh2c1
+fG723NTDbq/QwyPxLc+UVW355cSKrrf1C0rz5m6s1J0e196WSYviu+LvbrwhWf3YdyY6DrZOVLy+
+dWLWiudjQnFLv9/ln7ewcHry9ZvP8xbkrbjmzpeWAq3/JwIem9PwPvnH5jT8atkF4poZNz+oaJ5i
+zMO/WvHkFv+K+JGmTFVEt3UPmiKX0cHSwVR5enMb90+rsJwvbjZiv92cWzC/2Nyzd4pwTFcrRtud
+rtIipcNRwfNPrPty2bzvfU7XhNHXfA8A4sMSP9n1h2bNnOXxWC+tuChXW7l4rtwfvFgc6XiC6slT
+cRtNuANeKC7CEchjPJPHjvYs61/Yz8TEBG/dVofx758hXF/P71Y/yn0/biY/z89PC17h9E+ezapX
+NN5+u43LLpm95KWnVu9+8dIruOLlFz9cBU52/cHGWV+tq0/fu3zxsN9T4pP3JibEbrGNpuWfoHfb
+MHXP7qIglaXYSOH2aMzwuliwfBn/vv4qmkMaCTsU1NaS7uriXL2HNQ7BF+ZP0DQRYps+j+ZD78m7
+7jhXXOro3nj2ek/dFS+/mNv7263/fAUem9PALYfaeahhRpUlbL8+66zJ+fNnDCsR3SXPLVsicmfH
+edG5kTZxkAF7D/dfGGa8305RfoDbb6rjzHpBQftRAh439rIgRkUFyd3vMfHJT6Lc8gX6/+0bVO3c
+iDJvLm1KFUF7AuMXj8r+N7cI0+Fcf/WB/avX8iEitKa8weYvVC/wB8wN568YcRbmReQbtnJxw4yZ
+LF6mcpl1BXfYvsgt2dvZYn+Njo02Cr73SR7/WiPTnnua4uc24gFUwASyQAZI6TpRrxdWnotVVU0m
+lSLb2YG56110TWPM7ZbZYDBZfONnLjrj8zdv+7sFnHAc4L6SOYvrGs2vNNZNXbNw5hgdDp/8edFs
+sX5RlFztYVLksAD9j8RUE3IReHzD/dT/56/Ib22lJJ3GW1qCvaIM7C6ElBjRKMmhIRKxGBOaxkQm
+g6FpKKqKXdPwz5iB49KL6Zl7DmtfGXj1xafuu/IfqsC3g7ML8vLlYwsXxS6fO2PE6XbmeMTVJJ+a
+XyA6TtmF0DOc3KmFBAnICTBH4bTtJdx2l4on3075XV9hsLiB9w7n+MyqOXg7O1D370eefRaT132S
+ia4u/E8/jczlMDUNs7oaW36AfSE7a5/dz8jIBKee0fj/FxBgId+YmSzN8+c+W1aWvW/p4gl87rjs
+Uf3i0xWL2L94lHTJIYTkrwZSSpAJkN3g6dT4/lNLqLv1O/z0v8d55c0YT90/lytyHbhv/RI7Pvdt
+Nul1rMlsYewHP8b2gx8iP387Ohbx377EyEMPoGSSBCrKpebxiIFkNvKBW54clTs9c2ua5iVunlE7
+9W81NbnS4qIYaUuRd/sWi2cWuWH2eySIIgDlbwn449b7dB5Fm4KcU305r+7O42i3wac/fSp3lh6l
+5uvfYO2nHuXLv83iEhG2f3M6wc9fT7SoiF9/YS1P/rqbhQvrmNFQTFCLyhIRES6RRsmGNv9ZGz2Z
+/EONDT88Ze7QZ+atmJbnLGnA1veqxARdscRHc0NwsIhnes/GMS2Ms7GFsDqJ+GNsThYiLAj8v2L8
+jxaiWhpv9b9Dz7EF5OcXsmqxQuWN3+CFL/6IOzb7EFaKeC7LpmiQ/1i4ELl1G9fqXfw4nuOXGw+h
+6D5sbj8u3bR8yv4HKwpa1oiTia9pmqMVmJmVvlLr2TMvL8gv/sjXpNW8llTLgMhEC1CLA4ixblze
+IRz2ENgsnnfX8lPHPEZmhUg0dJFxhzDU7PtrAAGBR4rJe6IQoSjYbIK2kTlcd/UFfEvdQyaRZvme
+BVhjW1GNLhQ1hb/Qz+w8G0V9fZTOn8ProQDdHTFKpcUZyihpR/+atT0D93CyV4/OaWz0qca9p1jh
+62fPnoBrTpVS9YuJZw7jvHAl3vOKIW2AJUh3J8kdHcYa6cOVOIpek+QtWwWb9HJeawgwXD+AWtJB
+TuQQgK3fTvHVldgSdoRNRwnDjx76CWUPfpcjsxchjrbhCg0SGhohIUEEg9hqanGUlmKzLNzHBvHs
+34s2HkKgEMkPPHH+wPDnD3C8DQNwSUnhYpsmf7hEjuEcMqXoHxaCHhzuDMa+OOkpBb3Mh+JQ0Pwq
+9po89LoKcv4aQm9o1GZGuLSlm08804+/v5Dd8jR8tkKsvAFyfoNzj1zEYMcgp8fiXCbTaIU+HKFJ
+FhcHmBssoVCo+KMx8qemqMwZNOXnM7+sjLkVFZS7PIhUhmRoEkuCLZOrvs4seOl0KxZS4fj7zC2H
+jnTWXP/I5sNa4LMJKyW84ym05hxqu4VjdAJ7czuJDYeIdSaRloLitaO6dPQyN97za8mmKphqs+PO
+pjn7YB+3bu0kNuSle3IpLqeH3U3vcMETGh8dHSKgafgjUWrLg3jcHnS7HVXVMNJpMtEYmXgchILu
+caP7fOguF1JK0pOT5GJx9BLLXX5L0v3lB6pfVgFeGQ1x7+cfIFy3LJt/6tIbQjVLvYeKmthrK2Vc
+CEQqjFczcCkGrqFjWDs7Sb3dTzqUw1Ds2Aod2KZ78KyoxGysIVVdjlQcrHxvkNVvHaF4TJAXreHM
+zj7sowaWkcOr6zgcPnSvDb1CQSsTCL+BKeLkUlGMRAYhHdicfuweP6ruwLKHUWsGKbwuiaPSnK+E
+5JY/a3w3fXe7r6bM+fv6SueiRNrCJkyy8ST9R/px73uFJQOvs9gM4ciYIMBEIVOaR7RyGvZZtQQu
+KwNNgCmxMhYybZBpm+DYhkNMGL1MTWaIH9JQhMTu8lC/qprSjxXiKMlHd7mwMknSQ90k+7vIJeLo
+ngIchY04KxpR/Cqp4TaSfe8hc0mJREih9KknutCrYxPMvvgr9uo8+dmAWy/OGRYel0ZG6pTXl+I/
+fQXvll9Cc9pFyoxSmImiCxN7OIXHcQzpGyS0fhgrqaKXulAcGopLQ6/24Vs2jZ6NYcyxcdI5gYag
+YrVB4II0qsuB6rQj7DZUhx1UiZRJMOOgZxFeUAttaPk+FKeKlYphxMJCCDBMaanH89/OY03TlfzQ
+4Y85F52z2ufSdIemoGsqhinxOFWSySzlQTeOuUvpKj+Dfc4qRhJp7JkQgSkD29EM3vgk7Okh+Yc+
+0mELU9hId8VIvNBGeX87SdOgU7fTdKMk/zQwkyZCUVDtTmwuB4rLieqy4yCLyEUgE0fBQtF1NK8X
+1eOVNtUiGwmLsfH0wa2tXP9+hNY21W5NV8xZVPPNRx3Rqbi060JYlqQkYCOTk4xHMqgapFIWDodK
+NGHgMJMM7HyPkp3rOC3WziwRRzGOP3tNFNIeP4o0ccSjZJ02njdK6E0YfOS2Sdw+iZQSzenEFaym
+o01nqCXOWFrQZyvFmVIo0qL48qYoqbNRVNKIGqimtdMgO7Rh9Ztvhl5dt5XQ+wJWXvjAg8vCW7/k
+9bvk9M9+WuQHizkWU9F0hWzWxO/R0FWF4Yk0pmUhxPH3HLtDYyKpMvLmGxQfeZkFkSPMiQ/hMHNo
+SKQQjOpufq+WcboSJhTwMzJnkgXzopiWYLJPoefZDEUxSVuwiecKb0EZPheRcSLm9XLhdS4O94bY
+cvd0HNkytKZdqUMtZ78/JNAAzr3oITGl+ft3V30lWjSa9A1/u4WCacdonCOxgo3Yak/B7vYTCsXx
+OjWkBEWFqVgOTIs8m0nFVRfQN7Kc7V1H2N7fQlHXHqpCrajkcFsG1yU6MaVKiU0wOpVPJBZDpvLw
+PJfldF3y/OKr+J3xRQIDS8iVhFh4fZSzljWR73Vj2Y+yy1aIlrXjLIn20PKnxqMtuexp/vDSpyTw
+wB0/+EVwdMR+x8HuJopGr6azN0K5bxOV9tsYLq7Gfd71FM9sJBpJoFkSt13DblPIGRaKaVKep6Eu
+nUOosRHFupwdg1Gq/utL3Bg/yO7GEvLPzaL9EpYUOhmbrCLYb+OoPc1jTTfRfexyXP1NTM4+yPW3
+5DGzthQQCCzajoRR4zOl6gsLqWS3n9w51aGOTe//uPLqq6yFcwtWFQQnGFKPELO5GDn2EY7GbiKT
+8CO2/ycj218jozvJ6k5Um4OsISkvchBPmuiagmlKBBK3x4awuRiNK5T2vEv57VnK56SJjtnwhlw4
+Yzpb9/l4eu7tDPdcTLqtkdQ573DrHRVMr8xHAvFUlueeG2bnd6ajqjlsC94NicDoZwcPbw79WYRO
+Ql9kKsL08gB1FYKewV5GTh2m90ABXQN1DBjPkhcZoej53xAoXY9V50DOuwBha8AGuG0KmYRFkd/O
+aCRDwC2IzF5IZJuDdK+K4U7DgGS8O8orJafx1vRroOV80kaaipv2s/qGRvwuBwhJc+cQLz1oo/fV
+SvzBIdIrd8TG7Ucu6X7yB0eWXr6OXZtXf1CAwLTrih2XrjMVjTJ7eiVV0QjTy0YZDo2yZ+8uxibr
+SYZuYnDIoCC+j/rWnzBc7CHvY7dgqwoCBqmsxLSg0K+iOl1Il4fCjZNkN0EsAesar+Fg9jy8recz
+VHKIy2+2c9biOhShYEnJC6938of7KjBHfAQWb2Po3hZCNSGRfOCoDXifPIC69PJ1DLZvAjaK+nnZ
+L8+bWbIMaWLXdTQkqVSKYFkZZcVOaiviuIrGGBAHyTkMYtEm+lOrEDkv+lv3c6y3l6KaMrIoGKik
+c5JIOElB2xuEK/ykRwUPlH2G3tS1ePqXEWts4ea78lkyJwgSMobBM8/18No3S7BFPHgvfZHRr3bQ
+c/EwObchbb/nDbN5b9vJpmu7Nq9myWVPF5pm4gFVVW9w6aqcmIyJsrIg4XCEyvIyUukUU+EwhUVB
+3K4oM2ss2ns7GQr1cKw7wKGhafQ6f0mwtZVI23Y81SHKGzRSBXVkUzq2eIieznJeafgqwcKPEH6n
+AOWsg9y0qoBpJXlIJN0jUzzzcISu52tw5ycIXrqZFUqYjgE4lvs9OatMcXnnO7J/kXkNQCoyqVmK
+a3gQRqI5lsyfy+Ej7SiKSjKRIJVOUVVRRiqdJpzJous2muqLaKyxiM5KMTjRRnNzM20TBQSyV+Lp
+LqZ/sI8S9+v40vvYlHc2eed/irPm1VE67ufNPR08cls1Ds2GTVP53bs9bPiWjnFgNu5p3dResIsr
+zRhLL5rH1t4uNsbryOoJLCNn8dcE7H7xxiRfv+Y3wUDDVcM7DPH2gRGuOms2wSIvx4YGcNgdJJNJ
+otEoddWVjIcmQChkU0nKSwoIuGM01ToZCiU40LaV0ZCOPtnAROJTpDwXYU36udblI1g0SoYwtrSL
+WCKLcCk8sa6DHfeX44kVoy5uZsbKPdyYsDPzksUIVaFCuJESRLrKiEZGIn9VAFefKrSs+9PRC7Ls
+ntUl2/d6xI4dURYMF7K4Ksi8UwLkeQUF+QGOHRsFJIqQFAbyyGRSeD1u7HYHNgTTLyxkKhxhLNzP
+gfaDRPY34h+dRfNbGW67yka7laJYFPPKuy0MvRGk+/m5+GxZzOveoOt7u3COGFRsOw00BRDoUuDU
+fyNzzacaWev0/r8UoHLPalj7Gta8gg0rww1p62ByxXh9UiYuNsXBJeN0hGK0Ppvk0MEQvgJBU+M0
+XE4nx46N4HK5sKRFcWERofExPB4XuUwGhEJVWSFOJUJrTxJncgaRMZ0zLk8SNzIkBvxs+XEl0YMl
+OIPd5G79HS0/2UO6yCDlPMQ575RTUhmErMX2sTflH/LHRfTlU/fKR55Y80EB2w4c33uvx/jaTbcP
+nO2u+ZLvvaxIvRlGiUpGzohx5NoRUjaVd19MsHt3Hy5dYdGC2UxOhtA1jcnJKVwuF16Pl2Qigdvt
+ZnJqCq/Xw9GBKNmpUiY7/dQviuMsypAadXJgq4L7nN8zfu9Ojtx6FKkKEBru3BssPNRAftYvf/3W
+a+LBpr1yZMuMtfJ7T30UgHtW8z7nk58D33joSfr6utIOXZXnV9eJ5ZkqRiJp+jbG2esYpaWmG/l1
+B6FQIe07Urz28ChLaouZM9dJMOhHShgaGiaQH8CSoGo6eT4vbtcAx+IRYolyOvfbWTJLYSzZj3Zt
+K1239TO+MAJS5/h8QWLaBmizd8pN8Tbx2hmjIfZMu5WfPPnf3L0KhIDvr/uLCgDcvYq3936XGxpv
+8Eub+IpiZsnkMgTzAgQMi/l6IStzDSTfneLwQA+ZZSZtHw8zGMtw+IUM/V1T5BerzJs9A8sw6Rsa
+xm63k0on6R6IMtBaBpOlBAoshlMtbOEoLd8/SqwmBYAQU6hiCEVmUfRm2QqitaNwnzxcvIyHntwF
+wPYDf+b8nwTcvQrWrIfzVjiSNs/6c6rqZ5imIR26KgwjS87IUVJUQjYaZo4jn0vymxh5Z4hMc5xw
+U4rO1SEmNIW9L6doPtCDTVOprqlBFZDLpTnYEWfiaCUiXMCxgp3sW3KYljt7MG1pNNGJxIbDOICq
+vCcVMSGU1gIrunHJs/K+dRey+/L4X0bmLyH+mKtpWOaGG0sXLL62fqbs6u8RZWUVxOMxAoEAhmEw
+PjGF0+1hanKK/Lx8+qNh2q0kuxODtNWHccz0k5cswNliY1qHi8ZUASUlDt5pDhFurUfOGqX/lnbG
+zowhUXCZL6Oo+5GAJRulNDXBTls8+8KZd1kPPfT432T8gQoALJtnIcSVg9lorV9xi/NOXSqzqbiY
+ikRAQjqdJj8vD6ddJxqLkc6k8btcVKh2lgQqOMuqZ2znCF29fTDdYvBjaVoXTjLWapAMZcgunaLt
+W+1EZuaYdnAmujWF4dtwfHIHUpEhIX/b0JXbOnuF9aMnXjsRa7b/bef/VIETETp+0nIs87tBzTfn
+ospZ/iubFggrGiUZiUlNU0UiEcfn9ZHJZsnlsqTSGQoKC4lGo3jcPlKajc2dB9ifGidbqBMp6cC4
+6jdYBLHMldjF82TEZQjRg409xwePSbe0Nq3cmVxz2qW0/McU96z+wEL93ytwQuVxIX3sOPRU/PT6
+V/ZN9HVtaD9gHo5MVhlOh1bkysNnd2K3aYSnpnC53EjLQtNUpGnh93tJTE6wuKKW80rryZuy2D0Q
+RjvrAELmMJQgKp1I6UMXrVKQE3Jvk5F7+Yzvp2964QbGzkyLez/1D5E/LuAEth/4U9l2tIzxTutO
+ubzpNyPJyZ/tnxo5tGWkq7o3Fg06DRs1ZZUyFplAIIRhGgTLgmQyGXK5HBKL0NQkNYEi9g2OEA5O
+oBSPgjGblHoRquiVejQsMr+4OJfZV/VR885frgXBP+r8BwWcEHEyli8wUNSEXLPuQGZr88/7FxSt
+eysyEDw6Pl6Z58h3lBcXY2WzmIYhY7G4KCoqxDRypDI5VCHoGB9iQLNQZ3UilCPosgPtaEYkn7ik
+3xzXFrHmv97l7p6/2SL/Hvzvn1lPOHL3quPbNet7gY8333HF9ObUyCeaRoquWVBSPet0rUyUFxRJ
+BcRUOILD4UYVCm5FRYYCAFIBzDfrRGLrzF/JkPgcP1sf/bP190/iw33ovuvjAbvqWm5T1B8v8JVN
+v7SkgVNq6qU0sqKns4MNA0fYNu2otN/0a5F96gqyXXl38uy0++n9lvxnI/OvFXAy7rrubIT69Rm2
+wlOWF9b6F5XXqo+1bqN9+iFkSotkW4qv4YFf/O5f4frJ+PAC7ll9fMJ1gtSdVzcixQqv7jsziTnL
+UsywnHDcxs9+vu9fTf5fixPr5ATuvEbn7hvyuGu15//ytv8D3R+DwEWbsvsAAAAASUVORK5CYII=
+"
+ id="image10"
+ x="0"
+ y="0" />
+</svg>
diff --git a/images/gplv3-88x31.png b/images/gplv3-88x31.png
new file mode 100644
index 0000000..ba78d4c
--- /dev/null
+++ b/images/gplv3-88x31.png
Binary files differ
diff --git a/images/gray-cross.png b/images/gray-cross.png
new file mode 100644
index 0000000..794cffd
--- /dev/null
+++ b/images/gray-cross.png
Binary files differ
diff --git a/images/green-tick.png b/images/green-tick.png
new file mode 100644
index 0000000..0cd9009
--- /dev/null
+++ b/images/green-tick.png
Binary files differ
diff --git a/images/green-triangle-up.png b/images/green-triangle-up.png
new file mode 100644
index 0000000..d5963fc
--- /dev/null
+++ b/images/green-triangle-up.png
Binary files differ
diff --git a/images/info.png b/images/info.png
new file mode 100644
index 0000000..96642db
--- /dev/null
+++ b/images/info.png
Binary files differ
diff --git a/images/language.png b/images/language.png
new file mode 100644
index 0000000..d9fae8e
--- /dev/null
+++ b/images/language.png
Binary files differ
diff --git a/images/mainicon.ico b/images/mainicon.ico
new file mode 100644
index 0000000..5097078
--- /dev/null
+++ b/images/mainicon.ico
Binary files differ
diff --git a/images/new-topright.png b/images/new-topright.png
new file mode 100644
index 0000000..165e998
--- /dev/null
+++ b/images/new-topright.png
Binary files differ
diff --git a/images/next.png b/images/next.png
new file mode 100644
index 0000000..09a97b9
--- /dev/null
+++ b/images/next.png
Binary files differ
diff --git a/images/openbook-24.png b/images/openbook-24.png
new file mode 100644
index 0000000..dd0986e
--- /dev/null
+++ b/images/openbook-24.png
Binary files differ
diff --git a/images/orig/Aa.xcf b/images/orig/Aa.xcf
new file mode 100644
index 0000000..5685477
--- /dev/null
+++ b/images/orig/Aa.xcf
Binary files differ
diff --git a/images/orig/RX.xcf b/images/orig/RX.xcf
new file mode 100644
index 0000000..d1586b6
--- /dev/null
+++ b/images/orig/RX.xcf
Binary files differ
diff --git a/images/orig/blue-triangle-down.xcf b/images/orig/blue-triangle-down.xcf
new file mode 100644
index 0000000..876588c
--- /dev/null
+++ b/images/orig/blue-triangle-down.xcf
Binary files differ
diff --git a/images/orig/card.xcf b/images/orig/card.xcf
new file mode 100644
index 0000000..8d60dd3
--- /dev/null
+++ b/images/orig/card.xcf
Binary files differ
diff --git a/images/orig/chart-future.xcf b/images/orig/chart-future.xcf
new file mode 100644
index 0000000..90e2ce0
--- /dev/null
+++ b/images/orig/chart-future.xcf
Binary files differ
diff --git a/images/orig/chart-past.xcf b/images/orig/chart-past.xcf
new file mode 100644
index 0000000..f4e803b
--- /dev/null
+++ b/images/orig/chart-past.xcf
Binary files differ
diff --git a/images/orig/green-triangle-up.xcf b/images/orig/green-triangle-up.xcf
new file mode 100644
index 0000000..1da3dce
--- /dev/null
+++ b/images/orig/green-triangle-up.xcf
Binary files differ
diff --git a/images/orig/question.xcf b/images/orig/question.xcf
new file mode 100644
index 0000000..5f711c2
--- /dev/null
+++ b/images/orig/question.xcf
Binary files differ
diff --git a/images/orig/selection.xcf b/images/orig/selection.xcf
new file mode 100644
index 0000000..6c7c845
--- /dev/null
+++ b/images/orig/selection.xcf
Binary files differ
diff --git a/images/orig/spaced-rep.xcf b/images/orig/spaced-rep.xcf
new file mode 100644
index 0000000..9d960b5
--- /dev/null
+++ b/images/orig/spaced-rep.xcf
Binary files differ
diff --git a/images/orig/whole-words.xcf b/images/orig/whole-words.xcf
new file mode 100644
index 0000000..5160c32
--- /dev/null
+++ b/images/orig/whole-words.xcf
Binary files differ
diff --git a/images/orig/word-drill.xcf b/images/orig/word-drill.xcf
new file mode 100644
index 0000000..5f9955c
--- /dev/null
+++ b/images/orig/word-drill.xcf
Binary files differ
diff --git a/images/passes.png b/images/passes.png
new file mode 100644
index 0000000..586dfe6
--- /dev/null
+++ b/images/passes.png
Binary files differ
diff --git a/images/pencil.png b/images/pencil.png
new file mode 100644
index 0000000..82ed03a
--- /dev/null
+++ b/images/pencil.png
Binary files differ
diff --git a/images/pie-chart-3d.png b/images/pie-chart-3d.png
new file mode 100644
index 0000000..13771be
--- /dev/null
+++ b/images/pie-chart-3d.png
Binary files differ
diff --git a/images/question.png b/images/question.png
new file mode 100644
index 0000000..fa5c5c8
--- /dev/null
+++ b/images/question.png
Binary files differ
diff --git a/images/red-cross.png b/images/red-cross.png
new file mode 100644
index 0000000..3abef06
--- /dev/null
+++ b/images/red-cross.png
Binary files differ
diff --git a/images/red-stop.png b/images/red-stop.png
new file mode 100644
index 0000000..033fc67
--- /dev/null
+++ b/images/red-stop.png
Binary files differ
diff --git a/images/remove.png b/images/remove.png
new file mode 100644
index 0000000..c03d56b
--- /dev/null
+++ b/images/remove.png
Binary files differ
diff --git a/images/selection.png b/images/selection.png
new file mode 100644
index 0000000..0ea4b97
--- /dev/null
+++ b/images/selection.png
Binary files differ
diff --git a/images/spaced-rep.png b/images/spaced-rep.png
new file mode 100644
index 0000000..32a0098
--- /dev/null
+++ b/images/spaced-rep.png
Binary files differ
diff --git a/images/statistics.png b/images/statistics.png
new file mode 100644
index 0000000..67e7818
--- /dev/null
+++ b/images/statistics.png
Binary files differ
diff --git a/images/study-settings.png b/images/study-settings.png
new file mode 100644
index 0000000..77f1cad
--- /dev/null
+++ b/images/study-settings.png
Binary files differ
diff --git a/images/up.png b/images/up.png
new file mode 100644
index 0000000..a5b0944
--- /dev/null
+++ b/images/up.png
Binary files differ
diff --git a/images/warning.png b/images/warning.png
new file mode 100644
index 0000000..d83f349
--- /dev/null
+++ b/images/warning.png
Binary files differ
diff --git a/images/whole-words.png b/images/whole-words.png
new file mode 100644
index 0000000..59aeb7d
--- /dev/null
+++ b/images/whole-words.png
Binary files differ
diff --git a/images/word-drill.png b/images/word-drill.png
new file mode 100644
index 0000000..4615f97
--- /dev/null
+++ b/images/word-drill.png
Binary files differ
diff --git a/packaging/FileAssociation.nsh b/packaging/FileAssociation.nsh
new file mode 100644
index 0000000..157257f
--- /dev/null
+++ b/packaging/FileAssociation.nsh
@@ -0,0 +1,190 @@
+/*
+_____________________________________________________________________________
+
+ File Association
+_____________________________________________________________________________
+
+ Based on code taken from http://nsis.sourceforge.net/File_Association
+
+ Usage in script:
+ 1. !include "FileAssociation.nsh"
+ 2. [Section|Function]
+ ${FileAssociationFunction} "Param1" "Param2" "..." $var
+ [SectionEnd|FunctionEnd]
+
+ FileAssociationFunction=[RegisterExtension|UnRegisterExtension]
+
+_____________________________________________________________________________
+
+ ${RegisterExtension} "[executable]" "[extension]" "[description]"
+
+"[executable]" ; executable which opens the file format
+ ;
+"[extension]" ; extension, which represents the file format to open
+ ;
+"[description]" ; description for the extension. This will be display in Windows Explorer.
+ ;
+
+
+ ${UnRegisterExtension} "[extension]" "[description]"
+
+"[extension]" ; extension, which represents the file format to open
+ ;
+"[description]" ; description for the extension. This will be display in Windows Explorer.
+ ;
+
+_____________________________________________________________________________
+
+ Macros
+_____________________________________________________________________________
+
+ Change log window verbosity (default: 3=no script)
+
+ Example:
+ !include "FileAssociation.nsh"
+ !insertmacro RegisterExtension
+ ${FileAssociation_VERBOSE} 4 # all verbosity
+ !insertmacro UnRegisterExtension
+ ${FileAssociation_VERBOSE} 3 # no script
+*/
+
+
+!ifndef FileAssociation_INCLUDED
+!define FileAssociation_INCLUDED
+
+!include Util.nsh
+
+!verbose push
+!verbose 3
+!ifndef _FileAssociation_VERBOSE
+ !define _FileAssociation_VERBOSE 3
+!endif
+!verbose ${_FileAssociation_VERBOSE}
+!define FileAssociation_VERBOSE `!insertmacro FileAssociation_VERBOSE`
+!verbose pop
+
+!macro FileAssociation_VERBOSE _VERBOSE
+ !verbose push
+ !verbose 3
+ !undef _FileAssociation_VERBOSE
+ !define _FileAssociation_VERBOSE ${_VERBOSE}
+ !verbose pop
+!macroend
+
+
+
+!macro RegisterExtensionCall _EXECUTABLE _EXTENSION _DESCRIPTION
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+ Push `${_DESCRIPTION}`
+ Push `${_EXTENSION}`
+ Push `${_EXECUTABLE}`
+ ${CallArtificialFunction} RegisterExtension_
+ !verbose pop
+!macroend
+
+!macro UnRegisterExtensionCall _EXTENSION _DESCRIPTION
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+ Push `${_EXTENSION}`
+ Push `${_DESCRIPTION}`
+ ${CallArtificialFunction} UnRegisterExtension_
+ !verbose pop
+!macroend
+
+
+
+!define RegisterExtension `!insertmacro RegisterExtensionCall`
+!define un.RegisterExtension `!insertmacro RegisterExtensionCall`
+
+!macro RegisterExtension
+!macroend
+
+!macro un.RegisterExtension
+!macroend
+
+!macro RegisterExtension_
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+
+ Exch $R2 ;exe
+ Exch
+ Exch $R1 ;ext
+ Exch
+ Exch 2
+ Exch $R0 ;desc
+ Exch 2
+ Push $0
+ Push $1
+
+ ReadRegStr $1 HKCR $R1 "" ; read current file association
+ StrCmp "$1" "" NoBackup ; is it empty
+ StrCmp "$1" "$R0" NoBackup ; is it our own
+ WriteRegStr HKCR $R1 "backup_val" "$1" ; backup current value
+NoBackup:
+ WriteRegStr HKCR $R1 "" "$R0" ; set our file association
+
+ ReadRegStr $0 HKCR $R0 ""
+ StrCmp $0 "" 0 Skip
+ WriteRegStr HKCR "$R0" "" "$R0"
+ WriteRegStr HKCR "$R0\shell" "" "open"
+ WriteRegStr HKCR "$R0\DefaultIcon" "" "$R2,0"
+Skip:
+ WriteRegStr HKCR "$R0\shell\open\command" "" '"$R2" "%1"'
+ WriteRegStr HKCR "$R0\shell\edit" "" "Edit $R0"
+ WriteRegStr HKCR "$R0\shell\edit\command" "" '"$R2" "%1"'
+
+ Pop $1
+ Pop $0
+ Pop $R2
+ Pop $R1
+ Pop $R0
+
+ !verbose pop
+!macroend
+
+
+
+!define UnRegisterExtension `!insertmacro UnRegisterExtensionCall`
+!define un.UnRegisterExtension `!insertmacro UnRegisterExtensionCall`
+
+!macro UnRegisterExtension
+!macroend
+
+!macro un.UnRegisterExtension
+!macroend
+
+!macro UnRegisterExtension_
+ !verbose push
+ !verbose ${_FileAssociation_VERBOSE}
+
+ Exch $R1 ;desc
+ Exch
+ Exch $R0 ;ext
+ Exch
+ Push $0
+ Push $1
+
+ ReadRegStr $1 HKCR $R0 ""
+ StrCmp $1 $R1 0 NoOwn ; only do this if we own it
+ ReadRegStr $1 HKCR $R0 "backup_val"
+ StrCmp $1 "" 0 Restore ; if backup="" then delete the whole key
+ DeleteRegKey HKCR $R0
+ Goto NoOwn
+
+Restore:
+ WriteRegStr HKCR $R0 "" $1
+ DeleteRegValue HKCR $R0 "backup_val"
+ DeleteRegKey HKCR $R1 ;Delete key with association name settings
+
+NoOwn:
+
+ Pop $1
+ Pop $0
+ Pop $R1
+ Pop $R0
+
+ !verbose pop
+!macroend
+
+!endif # !FileAssociation_INCLUDED \ No newline at end of file
diff --git a/packaging/clean-deb.sh b/packaging/clean-deb.sh
new file mode 100755
index 0000000..3569a8d
--- /dev/null
+++ b/packaging/clean-deb.sh
@@ -0,0 +1,4 @@
+sudo rm -r freshmemory_*
+rm packaging/debian/control.diff
+rm packaging/debian/desktop.diff
+
diff --git a/packaging/create-source-archive.sh b/packaging/create-source-archive.sh
new file mode 100755
index 0000000..a63842a
--- /dev/null
+++ b/packaging/create-source-archive.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+VERSION=`cat debian/control|grep Version| cut -d' ' -f2| cut -d'-' -f1`
+echo Version $VERSION
+BRANCH=`hg log -r v$VERSION|grep branch|head -n1| cut -d' ' -f7`
+echo Branch $BRANCH
+SRC_DIR=$PWD
+DST_DIR=freshmemory-$VERSION
+
+cd ..
+hg clone $SRC_DIR $DST_DIR > /dev/null
+
+cd $DST_DIR
+hg up $BRANCH > /dev/null
+rm -r .hg*
+cd ..
+
+tar cvjf $DST_DIR.tar.bz2 $DST_DIR > /dev/null
+
diff --git a/packaging/debian/changelog.Debian b/packaging/debian/changelog.Debian
new file mode 100644
index 0000000..09371c0
--- /dev/null
+++ b/packaging/debian/changelog.Debian
@@ -0,0 +1,3 @@
+freshmemory unstable; urgency=high
+ * See change log at fresh-memory.com or /ChangeLog file
+ -- Mykhaylo Kopytonenko <mishakop@gmail.com>
diff --git a/packaging/debian/conffiles b/packaging/debian/conffiles
new file mode 100644
index 0000000..45dfb78
--- /dev/null
+++ b/packaging/debian/conffiles
@@ -0,0 +1 @@
+/etc/xdg/freshmemory/freshmemory.ini
diff --git a/packaging/debian/control b/packaging/debian/control
new file mode 100644
index 0000000..f8d5564
--- /dev/null
+++ b/packaging/debian/control
@@ -0,0 +1,17 @@
+Package: freshmemory
+Priority: optional
+Version: 1.5.0-1
+Section: education
+Installed-Size: 2636
+Maintainer: Mykhaylo Kopytonenko <mishakop@gmail.com>
+Homepage: http://fresh-memory.com
+Architecture: amd64
+Depends: libc6, libstdc++6, libqt5core5a (>= 5.2), libqt5widgets5 (>= 5.2), libqt5gui5 (>= 5.2), libqt5xml5 (>= 5.2), libqt5network5 (>= 5.2), libqt5svg5 (>= 5.2), qttranslations5-l10n (>=5.2)
+Description: Flashcards application with Spaced Repetition method
+ Fresh Memory is an education application for studying languages
+ with Spaced Repetition method and flashcards. Its primary purpose
+ is to study and repeat vocabulary of foreign languages. But other
+ disciplines can be studied as well: history, geography, medicine,
+ mathematics. The study material is stored as collections of
+ flashcards.
+
diff --git a/packaging/debian/copyright b/packaging/debian/copyright
new file mode 100644
index 0000000..587e08b
--- /dev/null
+++ b/packaging/debian/copyright
@@ -0,0 +1,9 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Freshmemory
+Source: http://fresh-memory.com
+
+Files: *
+Copyright: 2015 Mykhaylo Kopytonenko <mishakop@gmail.com>
+License: GPL-3
+ /usr/share/common-licenses/GPL-3
+
diff --git a/packaging/debian/freshmemory.1 b/packaging/debian/freshmemory.1
new file mode 100644
index 0000000..00a9f12
--- /dev/null
+++ b/packaging/debian/freshmemory.1
@@ -0,0 +1,13 @@
+.TH man 1 "05 Oct 2015" "1.x.0" "freshmemory man page"
+.SH NAME
+freshmemory \- Flashcards application using Spaced
+Repetition method
+.SH SYNOPSIS
+freshmemory [dictionary-file]
+.SH DESCRIPTION
+Fresh Memory
+.SH OPTIONS
+No options
+.SH AUTHOR
+Mykhaylo Kopytonenko (mishakop@gmail.com)
+
diff --git a/packaging/debian/freshmemory.desktop b/packaging/debian/freshmemory.desktop
new file mode 100644
index 0000000..0159e95
--- /dev/null
+++ b/packaging/debian/freshmemory.desktop
@@ -0,0 +1,15 @@
+[Desktop Entry]
+Name=Fresh Memory
+Version=1.5.0
+Encoding=UTF-8
+Type=Application
+Comment=Flashcards application with Spaced Repetition
+Exec=freshmemory %U
+Icon=freshmemory
+Categories=Education
+X-Maemo-Category=Main
+X-Window-Icon=freshmemory
+X-Window-Icon-Dimmed=freshmemory
+MimeType=application/x-fm-dictionary
+Name[en_US]=Fresh Memory
+
diff --git a/packaging/debian/freshmemory.xml b/packaging/debian/freshmemory.xml
new file mode 100644
index 0000000..b6505de
--- /dev/null
+++ b/packaging/debian/freshmemory.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' encoding='utf-8'?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+ <mime-type type="application/x-fm-dictionary">
+ <comment>Freshmemory dictionary</comment>
+ <glob pattern="*.fmd"/>
+ </mime-type>
+</mime-info>
+
diff --git a/packaging/debian/postinst b/packaging/debian/postinst
new file mode 100755
index 0000000..78e82c3
--- /dev/null
+++ b/packaging/debian/postinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -e
+xdg-mime install --novendor /usr/share/freshmemory/freshmemory.xml
+update-mime-database /usr/share/mime
+xdg-desktop-menu install --novendor /usr/share/applications/freshmemory.desktop
+xdg-icon-resource install --context mimetypes --size 128 /usr/share/pixmaps/freshmemory.png application-x-fm-dictionary
diff --git a/packaging/debian/prerm b/packaging/debian/prerm
new file mode 100755
index 0000000..22fa72a
--- /dev/null
+++ b/packaging/debian/prerm
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -e
+xdg-mime uninstall --novendor /usr/share/freshmemory/freshmemory.xml
+update-mime-database /usr/share/mime
+xdg-desktop-menu uninstall --novendor /usr/share/applications/freshmemory.desktop
+xdg-icon-resource uninstall --context mimetypes --size 128 /usr/share/pixmaps/freshmemory.png application-x-fm-dictionary
+
diff --git a/packaging/freshmemory.nsi b/packaging/freshmemory.nsi
new file mode 100644
index 0000000..dc5096d
--- /dev/null
+++ b/packaging/freshmemory.nsi
@@ -0,0 +1,72 @@
+SetCompressor /SOLID lzma
+!include "FileAssociation.nsh"
+
+!define /file VERSION ..\version.txt
+
+Name "Fresh Memory ${VERSION}"
+OutFile "freshmemory-${VERSION}-setup.exe"
+
+Icon ..\images\mainicon.ico
+
+Page license
+Page components
+Page directory
+Page instfiles
+UninstPage uninstConfirm
+UninstPage instfiles
+
+LicenseData ..\COPYING
+
+InstType "Normal"
+
+InstallDir $PROGRAMFILES\Freshmemory
+
+Section "Fresh Memory (required)"
+ SectionIn RO
+ SetOutPath $INSTDIR
+ File ..\freshmemory.exe
+ WriteUninstaller $INSTDIR\Uninstall.exe
+
+ File /oname=README.txt ..\README
+ File /oname=ChangeLog.txt ..\ChangeLog
+ File /oname=AUTHORS.txt ..\AUTHORS
+
+ SetOutPath "$INSTDIR\tr"
+ File ..\tr\*.qm
+
+ SetShellVarContext all
+ CreateDirectory "$SMPROGRAMS\Fresh Memory"
+ Delete "$SMPROGRAMS\Fresh Memory\Fresh Memory*.lnk"
+ CreateShortCut "$SMPROGRAMS\Fresh Memory\Fresh Memory ${VERSION}.lnk" "$INSTDIR\freshmemory.exe"
+ CreateShortCut "$SMPROGRAMS\Fresh Memory\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Fresh Memory" "DisplayName" "Fresh Memory ${VERSION}"
+ WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Fresh Memory" "UninstallString" "$INSTDIR\Uninstall.exe"
+
+ ${registerExtension} "$PROGRAMFILES\Freshmemory\freshmemory.exe" ".fmd" "Freshmemory dictionary"
+SectionEnd
+
+Section "Qt libraries"
+ SectionIn 1
+ SetOutPath "$INSTDIR"
+ File qt-win\*.dll
+ SetOutPath "$INSTDIR\imageformats"
+ File qt-win\imageformats\*.dll
+ SetOutPath "$INSTDIR\platforms"
+ File qt-win\platforms\*.dll
+SectionEnd
+
+Section "Uninstall"
+ ${unregisterExtension} ".fmd" "Freshmemory dictionary"
+
+ RMDir /r $INSTDIR
+
+ SetShellVarContext all
+ RMDir /r $APPDATA\freshmemory
+ SetShellVarContext current
+ RMDir /r $APPDATA\freshmemory
+
+ SetShellVarContext all
+ Delete "$SMPROGRAMS\Fresh Memory\*"
+ RMDir "$SMPROGRAMS\Fresh Memory"
+ DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Fresh Memory"
+SectionEnd
diff --git a/packaging/make-deb.sh b/packaging/make-deb.sh
new file mode 100755
index 0000000..a7e7817
--- /dev/null
+++ b/packaging/make-deb.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+
+# Checks
+
+DEBIAN_DIR=packaging/debian
+VERSION=`cat version.txt`
+PKG_REVISION=1
+ARCH=amd64
+if [ `uname -i` != 'x86_64' ]
+then
+ ARCH=i386
+fi
+echo "Arch: $ARCH"
+
+DEBROOT=freshmemory_${VERSION}-${PKG_REVISION}_${ARCH}
+
+if [ ! -x freshmemory ]
+then
+ echo "Error: Executable 'freshmemory' doesn't exist"
+ exit 1
+fi
+
+if [ -x ${DEBROOT} ]
+then
+ sudo rm -rf ${DEBROOT}
+fi
+
+strip freshmemory
+
+lrelease -silent freshmemory.pro
+
+mkdir -p $DEBROOT/DEBIAN
+cp ${DEBIAN_DIR}/control $DEBROOT/DEBIAN
+cp ${DEBIAN_DIR}/conffiles $DEBROOT/DEBIAN
+cp ${DEBIAN_DIR}/postinst $DEBROOT/DEBIAN
+cp ${DEBIAN_DIR}/prerm $DEBROOT/DEBIAN
+
+mkdir -p $DEBROOT/usr/bin
+cp freshmemory $DEBROOT/usr/bin
+
+mkdir -p $DEBROOT/etc/xdg/freshmemory
+cp doc/config/freshmemory.ini $DEBROOT/etc/xdg/freshmemory
+
+mkdir -p $DEBROOT/usr/share/freshmemory
+cp --parents tr/*.qm $DEBROOT/usr/share/freshmemory
+
+cp ${DEBIAN_DIR}/freshmemory.xml $DEBROOT/usr/share/freshmemory
+
+# Copying Linux platform files
+
+DEBDOCDIR=$DEBROOT/usr/share/doc/freshmemory
+mkdir -p $DEBDOCDIR
+cp AUTHORS $DEBDOCDIR
+cp ${DEBIAN_DIR}/copyright $DEBDOCDIR
+cp README $DEBDOCDIR
+cp ${DEBIAN_DIR}/changelog.Debian $DEBDOCDIR
+gzip --best $DEBDOCDIR/changelog.Debian
+
+DEBMANDIR=$DEBROOT/usr/share/man/man1
+mkdir -p $DEBMANDIR
+cp ${DEBIAN_DIR}/freshmemory.1 $DEBMANDIR
+gzip --best $DEBMANDIR/freshmemory.1
+
+mkdir -p $DEBROOT/usr/share/applications
+cp ${DEBIAN_DIR}/freshmemory.desktop $DEBROOT/usr/share/applications
+mkdir -p $DEBROOT/usr/share/pixmaps
+cp images/freshmemory.png $DEBROOT/usr/share/pixmaps
+
+# Patching control file
+
+PKGSIZE=`du -s --exclude DEBIAN $DEBROOT| cut -f1`
+echo "Package size: $PKGSIZE"
+
+CONTROL_TARGET=$DEBROOT/DEBIAN/control
+CONTROL_DIFF=${DEBIAN_DIR}/control.diff
+cp /dev/null $CONTROL_DIFF
+
+VERSION_NUMSTR=`grep "Version" -n $CONTROL_TARGET`
+VERSION_LINE=`echo $VERSION_NUMSTR | cut -d: -f1`
+VERSION_STR=`echo $VERSION_NUMSTR | cut -d: -f2-3`
+echo "@@ -$VERSION_LINE,1 +$VERSION_LINE,1 @@" >> $CONTROL_DIFF
+echo "-$VERSION_STR" >> $CONTROL_DIFF
+echo "+Version: $VERSION-1" >> $CONTROL_DIFF
+
+SIZE_NUMSTR=`grep "Installed-Size" -n $CONTROL_TARGET`
+SIZE_LINE=`echo $SIZE_NUMSTR | cut -d: -f1`
+SIZE_STR=`echo $SIZE_NUMSTR | cut -d: -f2-3`
+echo "@@ -$SIZE_LINE,1 +$SIZE_LINE,1 @@" >> $CONTROL_DIFF
+echo "-$SIZE_STR" >> $CONTROL_DIFF
+echo "+Installed-Size: $PKGSIZE" >> $CONTROL_DIFF
+
+ARCH_NUMSTR=`grep "Architecture" -n $CONTROL_TARGET`
+ARCH_LINE=`echo $ARCH_NUMSTR | cut -d: -f1`
+ARCH_STR=`echo $ARCH_NUMSTR | cut -d: -f2-3`
+echo "@@ -$ARCH_LINE,1 +$ARCH_LINE,1 @@" >> $CONTROL_DIFF
+echo "-$ARCH_STR" >> $CONTROL_DIFF
+echo "+Architecture: $ARCH" >> $CONTROL_DIFF
+
+patch $CONTROL_TARGET < $CONTROL_DIFF
+
+# Patching desktop file
+
+DESKTOP_TARGET=$DEBROOT/usr/share/applications/freshmemory.desktop
+DESKTOP_DIFF=${DEBIAN_DIR}/desktop.diff
+cp /dev/null $DESKTOP_DIFF
+
+VERSION_NUMSTR=`grep "Version" -n $DESKTOP_TARGET`
+VERSION_LINE=`echo $VERSION_NUMSTR | cut -d: -f1`
+VERSION_STR=`echo $VERSION_NUMSTR | cut -d: -f2-3`
+echo "@@ -$VERSION_LINE,1 +$VERSION_LINE,1 @@" >> $DESKTOP_DIFF
+echo "-$VERSION_STR" >> $DESKTOP_DIFF
+echo "+Version=$VERSION" >> $DESKTOP_DIFF
+
+patch $DESKTOP_TARGET < $DESKTOP_DIFF
+
+# Access rights
+
+sudo chown root:root -R ${DEBROOT}
+sudo chmod g-w -R ${DEBROOT}
+sudo chmod -x ${DEBROOT}/etc/xdg/freshmemory/freshmemory.ini
+
+# Make deb
+
+dpkg-deb -b ${DEBROOT} ${DEBROOT}.deb
+lintian ${DEBROOT}.deb
+
diff --git a/packaging/make-wininstaller.bat b/packaging/make-wininstaller.bat
new file mode 100644
index 0000000..9b0d0ee
--- /dev/null
+++ b/packaging/make-wininstaller.bat
@@ -0,0 +1,15 @@
+cd ..
+lrelease freshmemory.pro
+
+cd eula
+call make plain
+
+cd ..\packaging
+
+cd qt-win
+call copy_qtdlls.bat
+
+cd ..
+"C:\Program Files (x86)\NSIS\unicode\makensis.exe" freshmemory.nsi
+
+pause \ No newline at end of file
diff --git a/packaging/packaging.txt b/packaging/packaging.txt
new file mode 100644
index 0000000..cf93b13
--- /dev/null
+++ b/packaging/packaging.txt
@@ -0,0 +1,66 @@
+Set Versions:
+ - version.txt - For qmake and Windows installer
+ - src/version.h - Only dictionary and study file versions
+
+What the scripts do:
+ - Generate translations: lrelease
+
+Prepare packages:
+ - Ubuntu 64-bit
+ - Windows
+ - Source archive
+
+Windows
+=============
+Required installs:
+- (Sphinx-doc)
+- NSIS Unicode
+ https://code.google.com/p/unsis/downloads/list
+- Set QTDIR=C:\Qt\5.2.1
+
+Compile:
+ qmake
+ make release
+
+Make installer:
+ Click make-wininstaller.bat
+
+
+Ubuntu Linux
+==============
+
+Required installs:
+ qt5-default
+ qtbase5-dev
+ qttools5-dev-tools
+ qttranslations5-l10n
+ g++
+ python-sphinx
+ git
+
+Compile:
+ qmake
+ make
+
+./packaging/make-deb.sh
+
+What script does:
+ * All file structure are created
+ * Changelog is zipped
+ * Architecture and Installed-Size are set in debian control
+ * Version is set in freshmemory.desktop
+ * Checks with lintian
+
+Clean:
+./packaging/clean-deb.sh
+
+http://www.linuxfordevices.com/c/a/Linux-For-Devices-Articles/How-to-make-deb-packages/
+
+
+Source archive
+=====================
+
+From the repository root:
+ packaging/create-source-archive.sh
+
+
diff --git a/packaging/qt-win/cleanup.bat b/packaging/qt-win/cleanup.bat
new file mode 100644
index 0000000..fb8324d
--- /dev/null
+++ b/packaging/qt-win/cleanup.bat
@@ -0,0 +1,2 @@
+del *.dll
+rd /s /q imageformats
diff --git a/packaging/qt-win/copy_qtdlls.bat b/packaging/qt-win/copy_qtdlls.bat
new file mode 100644
index 0000000..09b372d
--- /dev/null
+++ b/packaging/qt-win/copy_qtdlls.bat
@@ -0,0 +1,28 @@
+call cleanup.bat
+
+copy %QTDIR%\bin\Qt5Core.dll .
+copy %QTDIR%\bin\Qt5Gui.dll .
+copy %QTDIR%\bin\Qt5Widgets.dll .
+copy %QTDIR%\bin\Qt5Network.dll .
+copy %QTDIR%\bin\libwinpthread-1.dll .
+copy %QTDIR%\bin\icuin51.dll .
+copy %QTDIR%\bin\icuuc51.dll .
+copy %QTDIR%\bin\icudt51.dll .
+copy %QTDIR%\bin\libgcc_s_dw2-1.dll .
+copy %QTDIR%\bin\libstdc*-6.dll .
+
+set QTIMG_PLUGINS=%QTDIR%\plugins\imageformats
+set DSTIMG_PLUGINS=.\imageformats
+mkdir %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qgif.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qico.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qjpeg.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qmng.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qsvg.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qtga.dll %DSTIMG_PLUGINS%
+copy %QTIMG_PLUGINS%\qtiff.dll %DSTIMG_PLUGINS%
+
+set QT_PLATFORMS=%QTDIR%\plugins\platforms
+set DST_PLATFORMS=.\platforms
+mkdir %DST_PLATFORMS%
+copy %QT_PLATFORMS%\qwindows.dll %DST_PLATFORMS%
diff --git a/packaging/qt-win/list.txt b/packaging/qt-win/list.txt
new file mode 100644
index 0000000..283afa7
--- /dev/null
+++ b/packaging/qt-win/list.txt
@@ -0,0 +1,25 @@
+QTDIR = C:\Qt\5\5.2.1\mingw48_32
+
+%QTDIR%\bin:
+ Qt5Core.dll
+ Qt5Gui.dll
+ Qt5Widgets.dll
+ Qt5Network.dll
+ libwinpthread-1.dll
+ icuin51.dll
+ icuuc51.dll
+ icudt51.dll
+ libgcc_s_dw2-1.dll
+ libstdc++-6.dll
+
+%QTDIR%\plugins\imageformats:
+ qgif.dll
+ qico.dll
+ qjpeg.dll
+ qmng.dll
+ qsvg.dll
+ qtga.dll
+ qtiff.dll
+
+%QTDIR%\plugins\platforms:
+ qtwindows.dll
diff --git a/releases/1.0.1/README b/releases/1.0.1/README
new file mode 100644
index 0000000..977d52a
--- /dev/null
+++ b/releases/1.0.1/README
@@ -0,0 +1,61 @@
+Fresh Memory 1.0.1
+
+DOWNLOAD FILES
+=============================
+
+freshmemory-1.0.1-setup.exe Windows installer: 2000/XP/Vista/7
+freshmemory_1.0.1-1_i386.deb Linux deb-package: for Ubuntu and Debian
+freshmemory-1.0.1.tar.bz2 Source archive: for compilation and translation
+
+
+FEATURES
+=============================
+
+* Application supports both classical review of flash cards (Word drill) and efficient Spaced Repetition method
+* Flash cards are groupped into dictionaries
+* Import and export to CSV files
+* Display of simple studying statistics for each dictionary: number of scheduled and new cards
+* Flash cards can have rich formatted text and images
+* Flash cards can have several fields.
+* It is possible to define several card packs with different combinations of fields, and learn the card packs separately.
+* Cards with the same question are automatically merged into one card
+* Cards with several questions (separated with ';') are automatically broken down to several cards
+* Different card fields can have different styles: font and color
+* Adjustable background color and fonts of the flash cards
+* Automatic highlight of question keywords in the examples
+* Adjustable studying parameters: random or sequential card show, different day limits
+* Full support of Unicode (UTF-8): any international characters can be used.
+* Dictionary and its study data are stored in separate files. It is easy to share clean dictionaries.
+* Translations: Czech, Finnish, Russian, Ukrainian
+
+CHANGELOG
+=============================
+
+Version 1.0.1:
+
+* Bug fix: The study crashed on showing answer or next card, after dictionary was changed and saved.
+* Bug fix: Can't save study data for example dictionaries.
+ - Reason: the example dictionaries are installed at a system path, not writeable by usual user.
+ - Fix: The example dictionaries are copied to the user application data directory on the first application launch.
+ - Linux: ~/.config/freshmemory/dictionaries
+ - Windows 2000/XP: C:\Documents and Settings\<User>\Application Data\freshmemory\dictionaries
+ - Windows Vista/7: C:\Users\<User>\AppData\Roaming\freshmemory\dictionaries
+ - This path is set as default for opening a dictionary.
+* Filter *.fmd files in the Open dictionary dialog.
+* Show full dictionary path in the dictionary tab tooltip.
+* Show full file paths in error dialogs.
+* Fix the "Study progress" tooltip in Spaced repetition window: unreadable yellow color on non-Ubuntu systems.
+* Translations:
+ - Added: Czech (by Pavel Fric), Finnish, Ukrainian.
+ - Improved: Russian.
+* Source project file:
+ - Fix resource paths
+ - Fix paths for Windows
+ - Search "Known issue" in README file
+
+Windows:
+* Show file paths with native path separators ('\').
+
+Linux:
+* Packaging:
+ - Debian package: added "Installed size" field (makes Ubuntu Software Center happy)
diff --git a/releases/1.0.1/news.txt b/releases/1.0.1/news.txt
new file mode 100644
index 0000000..8a3742a
--- /dev/null
+++ b/releases/1.0.1/news.txt
@@ -0,0 +1 @@
+Maintenance release 1.0.1 is ready. It contains several small bug fixes and improvements. Added Czech, Finnish and Ukrainian translations. \ No newline at end of file
diff --git a/releases/1.0.2/README b/releases/1.0.2/README
new file mode 100644
index 0000000..0a3a23d
--- /dev/null
+++ b/releases/1.0.2/README
@@ -0,0 +1,17 @@
+Fresh Memory 1.0.2
+
+DOWNLOAD FILES
+=============================
+
+freshmemory-1.0.2-setup.exe Windows installer: 2000/XP/Vista/7
+freshmemory_1.0.2-1_i386.deb Linux deb-package: for Ubuntu and Debian
+freshmemory-1.0.2.tar.bz2 Source archive: for compilation and translation
+
+
+CHANGELOG
+=============================
+
+Version 1.0.2:
+
+* Fixed crash: With opened study window, closing the main window crashes the application.
+* Minor improvements in the study window.
diff --git a/releases/1.0.2/news.txt b/releases/1.0.2/news.txt
new file mode 100644
index 0000000..2d5ec53
--- /dev/null
+++ b/releases/1.0.2/news.txt
@@ -0,0 +1 @@
+Maintenance release 1.0.2 is ready. It contains one crash fix and several small improvements. \ No newline at end of file
diff --git a/releases/1.1/README b/releases/1.1/README
new file mode 100644
index 0000000..3265eb6
--- /dev/null
+++ b/releases/1.1/README
@@ -0,0 +1,58 @@
+Fresh Memory 1.1
+
+DOWNLOAD FILES
+=============================
+
+freshmemory-1.1-setup.exe Windows installer
+freshmemory_1.1-1_i386.deb Linux deb-package
+freshmemory-1.1.tar.bz2 Source archive: for compilation and translation
+
+
+CHANGELOG
+=============================
+
+Version 1.1:
+
+* Modifying records in the dictionary updates the shown cards at the study window
+* Save last used working directory in the user settings.
+* Can undo the following actions in the dictionary view:
+ - modify a record
+ - insert/paste records
+ - remove/cut records
+* Modern search pane in the dictionary view
+* Can edit the current card at the study view
+ - The editing view has the same context menu as the dictionary view: remove, insert, copy, paste, cut records.
+* Can delete the current card at the study view
+* The dictionary files (.fmd) are associated with Fresh Memory application. Can double-click a dictionary to open it.
+* Shrink row height and show grid in the dictionary view.
+* In "Recent files" menu: add path after file name
+
+Minor:
+
+* Changes in interface of the study window
+* "About" dialog:
+ - updated Qt icon
+ - added build date and revision
+* New study file format (1.1):
+ - save current card name
+ - save intervals of delayed cards
+* Change default study configuration:
+ - increase new card day limit to 20.
+* Linux deb-package:
+ - file association with .fmd dictionaries + application icon.
+ - enlarge main icon to 128x128.
+* Windows installer:
+ - application icon in the Start menu and Explorer
+ - file association with .fmd dictionaries + application icon
+* Project file:
+ - the default build mode is "release" for both Linux and Windows
+ - include generation of translations into installation
+ - Linux: register/unregister the .fmd file association in installing/uninstalling
+* Bug fixes:
+ - Don't use keyword highlighting inside HTML tags (e.g. <img>)
+ - Windows 7: Fix removing the Start menu links in uninstaller
+* Refactorings in the source code:
+ - Main window and application model
+ - fix relations between study models and views
+ - Copying dictionary configuration to/from dictionary options: copy study data together with packs.
+ - Two answer times: recall time and full answer time.
diff --git a/releases/1.1/news.txt b/releases/1.1/news.txt
new file mode 100644
index 0000000..d3edbdb
--- /dev/null
+++ b/releases/1.1/news.txt
@@ -0,0 +1 @@
+Next release 1.1 is ready. Now it is possible to edit cards from the study window and undo dictionary modifications. \ No newline at end of file
diff --git a/releases/1.2/README b/releases/1.2/README
new file mode 100644
index 0000000..d826968
--- /dev/null
+++ b/releases/1.2/README
@@ -0,0 +1,27 @@
+Fresh Memory 1.2
+
+DOWNLOAD FILES
+=============================
+
+freshmemory-1.2-setup.exe Windows installer
+freshmemory_1.2-1_i386.deb Linux deb-package
+
+
+CHANGELOG
+=============================
+
+Version 1.2:
+
+* Added card preview for dictionary records
+* Images:
+ - Display image thumbnails in dictionary view (instead of plain HTML tags)
+ - Add images with GUI
+* New study setting: Day starts at 3 o'clock (adjustable).
+ - Affects day limits for all and new cards
+* Updated icons for "Word drill", "Spaced repetition" and "Search"
+* Commercial version requires activation: online or offline.
+
+Windows:
+
+* Installer removes Start menu entries of previous versions
+
diff --git a/releases/1.2/news.txt b/releases/1.2/news.txt
new file mode 100644
index 0000000..a194e10
--- /dev/null
+++ b/releases/1.2/news.txt
@@ -0,0 +1,2 @@
+Next release 1.2 is published. Major features are live cards preview and adding images with GUI.
+
diff --git a/releases/1.4.4/news.txt b/releases/1.4.4/news.txt
new file mode 100644
index 0000000..9c19ed2
--- /dev/null
+++ b/releases/1.4.4/news.txt
@@ -0,0 +1,3 @@
+Version 1.4.4 is released.
+This is a minor release containing security updates.
+
diff --git a/releases/1.4.5/news.txt b/releases/1.4.5/news.txt
new file mode 100644
index 0000000..243b671
--- /dev/null
+++ b/releases/1.4.5/news.txt
@@ -0,0 +1,2 @@
+Version 1.4.5 is released.
+This is a minor release, which fixes several bugs.
diff --git a/releases/README b/releases/README
new file mode 100644
index 0000000..86f6223
--- /dev/null
+++ b/releases/README
@@ -0,0 +1,74 @@
+FRESH MEMORY
+http://freshmemory.sourceforge.net
+
+Fresh Memory is an application that helps to learn large amounts of any material with Spaced Repetition method. The most important subject is learning foreign words, but Fresh Memory can be also used to learn anything else. Other examples are country's capitals and flags, chemical elements, mathematical formulas, technical terms. The learning data is stored as flash cards and dictionaries: sets of cards. The flash cards may have several fields, and the user controls what combination of fields to learn. The flashcards can have formatted text and images. The look of flash cards and studying parameters are can be flexibly adjusted.
+
+FEATURES
+=============================
+
+* Efficient Spaced Repetition method as well as simple review of flash cards (Word drill)
+* Flashcards are groupped into dictionaries, which are stored in separate files
+* Import and export dictionaries from/to CSV files
+* Shows studying statistics: studied and scheduled cards for different periods, total new cards left.
+* Rich formatting of cards: colors, fonts and images
+* Images can be added with graphical user interface
+* Multi-sided flashcards. Dictionary records may have several fields.
+* Studying cards with different directions. Card fields are defined in card packs.
+* Cards with the same question are automatically merged into one card
+* Cards with several questions (separated with ';') are automatically broken down to several cards
+* Undo feature: all dictionary modifications can be undone
+* Editing and removing cards directly from the study view
+* Card fields may have different styles: font and color
+* Adjustable card background color and fonts
+* Automatic highlight of question keywords in the examples
+* Adjustable study parameters: random or sequential card show, different day limits
+* Adjustable day start: e.g. at 3 o'clock.
+* Full support of Unicode (UTF-8): any international characters can be used.
+* Dictionary and the study data are stored in separate files. It is easy to share clean dictionaries.
+* Translations: Czech, English, Finnish, French, Russian, Spanish, Ukrainian
+
+
+COMPILATION
+=============================
+
+Commands:
+ qmake
+ make
+ sudo make install
+
+See README file for details.
+
+
+USAGE
+============================
+
+freshmemory [OPTIONS] [DICTIONARY]
+
+Argument:
+ DICTIONARY - a dictionary filename to be loaded.
+
+Options:
+ --version, -v Prints version information and quits.
+ --help, -h Prints help page and quits.
+
+Dictionary files use .fmd extension. You can bind this extension to Fresh Memory for easy opening of your dictionaries.
+
+On Windows, output to console is possible only in the debug version. Compile it with:
+ make debug
+
+The user's study data (.fms files) is saved to the same directory as the dictionary.
+
+Changed settings are saved to user-specific application data directory:
+ * Linux and MacOS:
+ ~/.config/freshmemory/
+ * Windows:
+ %APPDATA%\freshmemory\
+ * Windows XP:
+ C:\Documents and Settings\<User>\Application data\freshmemory\
+ * Windows 7:
+ C:\Users\<User>\AppData\Roaming\freshmemory\
+
+Image paths in the dictionary records can be specified relative to the dictionary path:
+ - <img src="%/image.png"> # In the same directory as the dictionary
+ - <img src="%%/image.png"> # In <Dictionary dir>/<Dictionary file name without extension>/
+ - It is safe to move dictionary with its images to another place or computer.
diff --git a/releases/screenshots/fm-1.0-e.png b/releases/screenshots/fm-1.0-e.png
new file mode 100644
index 0000000..06357a4
--- /dev/null
+++ b/releases/screenshots/fm-1.0-e.png
Binary files differ
diff --git a/releases/screenshots/fm-1.0-f.png b/releases/screenshots/fm-1.0-f.png
new file mode 100644
index 0000000..c29bde3
--- /dev/null
+++ b/releases/screenshots/fm-1.0-f.png
Binary files differ
diff --git a/releases/screenshots/fm-1.1-a.png b/releases/screenshots/fm-1.1-a.png
new file mode 100644
index 0000000..96850a3
--- /dev/null
+++ b/releases/screenshots/fm-1.1-a.png
Binary files differ
diff --git a/releases/screenshots/fm-1.1-b.png b/releases/screenshots/fm-1.1-b.png
new file mode 100644
index 0000000..4d1ee5d
--- /dev/null
+++ b/releases/screenshots/fm-1.1-b.png
Binary files differ
diff --git a/releases/screenshots/fm-1.1-c.png b/releases/screenshots/fm-1.1-c.png
new file mode 100644
index 0000000..ef21944
--- /dev/null
+++ b/releases/screenshots/fm-1.1-c.png
Binary files differ
diff --git a/releases/screenshots/fm-1.1-d.png b/releases/screenshots/fm-1.1-d.png
new file mode 100644
index 0000000..c1c6dc6
--- /dev/null
+++ b/releases/screenshots/fm-1.1-d.png
Binary files differ
diff --git a/releases/screenshots/fm-1.1-e.png b/releases/screenshots/fm-1.1-e.png
new file mode 100644
index 0000000..b530bf3
--- /dev/null
+++ b/releases/screenshots/fm-1.1-e.png
Binary files differ
diff --git a/releases/screenshots/names.txt b/releases/screenshots/names.txt
new file mode 100644
index 0000000..102c4de
--- /dev/null
+++ b/releases/screenshots/names.txt
@@ -0,0 +1,8 @@
+Main window
+Word drill
+Spaced repetition
+Card editing
+New search pane
+Card pack configuration
+Font and color settings
+
diff --git a/src/charts/Chart.cpp b/src/charts/Chart.cpp
new file mode 100644
index 0000000..f50911a
--- /dev/null
+++ b/src/charts/Chart.cpp
@@ -0,0 +1,23 @@
+#include "Chart.h"
+#include "ChartScene.h"
+#include "ChartView.h"
+
+Chart::Chart():
+ scene(new ChartScene(this))
+{
+ createChartView();
+}
+
+void Chart::createChartView()
+{
+ view = new ChartView(scene);
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(view);
+ mainLt->setContentsMargins(QMargins());
+ setLayout(mainLt);
+}
+
+void Chart::setDataSet(const QList<DataPoint>& dataSet)
+{
+ scene->setDataSet(dataSet, 1);
+}
diff --git a/src/charts/Chart.h b/src/charts/Chart.h
new file mode 100644
index 0000000..a613b06
--- /dev/null
+++ b/src/charts/Chart.h
@@ -0,0 +1,32 @@
+#ifndef CHART_H
+#define CHART_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartView;
+class ChartScene;
+
+class Chart: public QWidget
+{
+public:
+ Chart();
+ void setLabels(const QString& xLabel, const QString& yLabel)
+ { this->xLabel = xLabel; this->yLabel = yLabel; }
+ QString getXLabel() const { return xLabel; }
+ QString getYLabel() const { return yLabel; }
+ void setDataSet(const QList<DataPoint>& dataSet);
+
+private:
+ void createChartView();
+
+protected:
+ ChartScene* scene;
+ ChartView* view;
+ QString xLabel;
+ QString yLabel;
+};
+
+#endif
diff --git a/src/charts/ChartAxes.cpp b/src/charts/ChartAxes.cpp
new file mode 100644
index 0000000..caf8828
--- /dev/null
+++ b/src/charts/ChartAxes.cpp
@@ -0,0 +1,138 @@
+#include "ChartAxes.h"
+#include "ChartScene.h"
+
+#include <cmath>
+
+const QPointF ChartAxes::XLabelOffset(0, 5);
+const QPointF ChartAxes::YLabelOffset(-10, -7);
+const QFont ChartAxes::TickLabelFont("Sans Serif", 11);
+
+ChartAxes::ChartAxes(ChartScene* scene, int xTickInterval):
+ scene(scene), xTickInterval(xTickInterval)
+{
+}
+
+void ChartAxes::paint()
+{
+ addAxisLines();
+ addTicks();
+}
+
+void ChartAxes::addAxisLines()
+{
+ QPointF chartOrigin = scene->getChartOrigin();
+ QRectF chartRect = scene->getChartRect();
+ scene->addLine(QLineF(chartOrigin, chartRect.topLeft()));
+ scene->addLine(QLineF(chartOrigin, chartRect.bottomRight()));
+}
+
+void ChartAxes::addTicks()
+{
+ addXTicks();
+ addYTicks();
+}
+
+void ChartAxes::addXTicks()
+{
+ qreal tickSpacing = scene->getDataDirection() * scene->getXTickSpacing();
+ QPointF pos = getTickOrigin(XTick);
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ {
+ addLabelledXTick(i, pos);
+ pos.rx() += tickSpacing;
+ }
+}
+
+void ChartAxes::addLabelledXTick(int index, QPointF& pos)
+{
+ if(index % xTickInterval == 0)
+ {
+ addTick(pos, XTick);
+ DataPoint point = scene->getDataSet().at(index);
+ addLabel(point.label, pos, XTick);
+ }
+}
+
+QPointF ChartAxes::getTickOrigin(TickOrientation orientation) const
+{
+ QPointF res = scene->getChartOrigin();
+ if(orientation == XTick)
+ {
+ if(scene->getDataDirection() == 1)
+ res.rx() += ChartScene::DataSetPadding.width();
+ else
+ res.rx() += scene->getChartRect().width();
+ }
+ else
+ res.ry() -= ChartScene::DataSetPadding.height();
+ return res;
+}
+
+void ChartAxes::addTick(const QPointF& pos, TickOrientation orientation)
+{
+ QPointF end = pos + getTickVector(orientation);
+ scene->addLine(QLineF(pos, end));
+}
+
+QPointF ChartAxes::getTickVector(TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return QPointF(0, TickLength);
+ else
+ return QPointF(-TickLength, 0);
+}
+
+void ChartAxes::addLabel(const QString& labelText, const QPointF& pos,
+ TickOrientation orientation)
+{
+ QPointF labelOffset = getLabelOffset(orientation);
+ QGraphicsItem* textItem = scene->addSimpleText(labelText, TickLabelFont);
+ qreal xAlignOffset = getLabelXAlignOffset(
+ textItem->boundingRect().width(), orientation);
+ textItem->setPos(pos + labelOffset + QPointF(xAlignOffset, 0));
+}
+
+QPointF ChartAxes::getLabelOffset(TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return XLabelOffset;
+ else
+ return YLabelOffset;
+}
+
+qreal ChartAxes::getLabelXAlignOffset(qreal labelWidth, TickOrientation orientation)
+{
+ if(orientation == XTick)
+ return -labelWidth / 2;
+ else
+ return -labelWidth;
+}
+
+void ChartAxes::addYTicks()
+{
+ int valueSpacing = scene->getYTickSpacing();
+ qreal posSpacing = scene->getYPosFromValue(valueSpacing);
+ QPointF pos = getTickOrigin(YTick);
+ for(int value = scene->getMinY(); value <= scene->getMaxY();
+ value += valueSpacing)
+ {
+ addLabelledYTick(value, pos, posSpacing);
+ if(valueSpacing == 0)
+ break;
+ }
+}
+
+void ChartAxes::addLabelledYTick(int value, QPointF& pos, qreal posSpacing)
+{
+ addTick(pos, YTick);
+ addLabel(QString::number(value), pos, YTick);
+ addHRuler(pos);
+ pos.ry() -= posSpacing;
+}
+
+void ChartAxes::addHRuler(const QPointF& pos)
+ {
+ QPointF end = pos;
+ end.rx() += scene->getChartRect().width();
+ scene->addLine(QLineF(pos, end), QPen(Qt::lightGray));
+ }
diff --git a/src/charts/ChartAxes.h b/src/charts/ChartAxes.h
new file mode 100644
index 0000000..ebfe4ac
--- /dev/null
+++ b/src/charts/ChartAxes.h
@@ -0,0 +1,51 @@
+#ifndef CHART_AXES_H
+#define CHART_AXES_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class ChartScene;
+
+class ChartAxes
+{
+public:
+ ChartAxes(ChartScene* scene, int xTickInterval);
+ void paint();
+
+private:
+ enum TickOrientation
+ {
+ XTick,
+ YTick
+ };
+
+private:
+ static QPointF getTickVector(TickOrientation orientation);
+ static QPointF getLabelOffset(TickOrientation orientation);
+ static qreal getLabelXAlignOffset(qreal labelWidth, TickOrientation orientation);
+
+private:
+ void addAxisLines();
+ void addTicks();
+ void addXTicks();
+ void addYTicks();
+ void addLabel(const QString& labelText, const QPointF& pos,
+ TickOrientation orientation);
+ QPointF getTickOrigin(TickOrientation orientation) const;
+ void addHRuler(const QPointF& pos);
+ void addTick(const QPointF& pos, TickOrientation orientation);
+ void addLabelledXTick(int index, QPointF& pos);
+ void addLabelledYTick(int value, QPointF& pos, qreal posSpacing);
+
+private:
+ static const int TickLength = 5;
+ static const QPointF XLabelOffset;
+ static const QPointF YLabelOffset;
+ static const QFont TickLabelFont;
+
+private:
+ ChartScene* scene;
+ int xTickInterval;
+};
+
+#endif
diff --git a/src/charts/ChartDataLine.cpp b/src/charts/ChartDataLine.cpp
new file mode 100644
index 0000000..17919cd
--- /dev/null
+++ b/src/charts/ChartDataLine.cpp
@@ -0,0 +1,80 @@
+#include "ChartDataLine.h"
+#include "ChartScene.h"
+#include "ChartMarker.h"
+
+const QColor ChartDataLine::MarkerColor(Qt::blue);
+const QColor ChartDataLine::FillColor(0, 0, 255, 128);
+
+ChartDataLine::ChartDataLine(ChartScene* scene):
+ scene(scene),
+ origin(getDataSetOrigin()),
+ xSpacing(scene->getXTickSpacing())
+{
+}
+
+QPointF ChartDataLine::getDataSetOrigin() const
+{
+ QSizeF padding = scene->DataSetPadding;
+ QPointF res = scene->getChartOrigin();
+ res.ry() -= padding.height();
+ if(scene->getDataDirection() == 1)
+ res.rx() += padding.width();
+ else
+ res.rx() += scene->getChartRect().width();
+ return res;
+}
+
+void ChartDataLine::paint()
+{
+ QPainterPath linePath = createLinePath();
+ addLinePath(linePath);
+ addAreaPath(linePath);
+}
+
+QPainterPath ChartDataLine::createLinePath()
+{
+ QList<DataPoint> dataSet = scene->getDataSet();
+ QPointF firstPoint = getDataPos(0, dataSet.first().value);
+ QPainterPath linePath(firstPoint);
+ for (int i = 0; i < dataSet.size(); i++)
+ addLineSegment(linePath, i);
+ return linePath;
+}
+
+void ChartDataLine::addLineSegment(QPainterPath& path, int i)
+{
+ DataPoint dataPoint = scene->getDataSet().at(i);
+ QPointF pos = getDataPos(i, dataPoint.value);
+ QString toolTipText = createToolTipText(dataPoint);
+ scene->addItem(new ChartMarker(pos, toolTipText));
+ path.lineTo(pos);
+}
+
+QPointF ChartDataLine::getDataPos(int index, int yValue)
+{
+ qreal xPos = scene->getDataDirection() * index * xSpacing;
+ qreal yPos = scene->getYPosFromValue(yValue - scene->getMinY());
+ return origin + QPointF(xPos, -yPos);
+}
+
+QString ChartDataLine::createToolTipText(const DataPoint& dataPoint) const
+{
+ return scene->getXLabel() + ": <b>" + dataPoint.toolTipLabel + "</b><br/>" +
+ scene->getYLabel() + ": <b>" + QString::number(dataPoint.value) + "</b>";
+}
+
+void ChartDataLine::addLinePath(const QPainterPath& path)
+{
+ QPen pen(MarkerColor);
+ pen.setWidth(2);
+ scene->addPath(path, pen);
+}
+
+void ChartDataLine::addAreaPath(const QPainterPath& linePath)
+{
+ QPainterPath areaPath(origin);
+ areaPath.lineTo(QPointF(linePath.elementAt(0)));
+ areaPath.connectPath(linePath);
+ areaPath.lineTo(QPointF(linePath.currentPosition().x(), origin.y()));
+ scene->addPath(areaPath, QPen(FillColor), QBrush(FillColor));
+}
diff --git a/src/charts/ChartDataLine.h b/src/charts/ChartDataLine.h
new file mode 100644
index 0000000..c90ccb4
--- /dev/null
+++ b/src/charts/ChartDataLine.h
@@ -0,0 +1,37 @@
+#ifndef CHART_DATA_SET_H
+#define CHART_DATA_SET_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartScene;
+
+class ChartDataLine
+{
+public:
+ ChartDataLine(ChartScene* scene);
+ void paint();
+
+private:
+ QPointF getDataSetOrigin() const;
+ QPainterPath createLinePath();
+ QPointF getDataPos(int index, int yValue);
+ void addLinePath(const QPainterPath& path);
+ void addLineSegment(QPainterPath& path, int i);
+ void addAreaPath(const QPainterPath& linePath);
+ QString createToolTipText(const DataPoint& dataPoint) const;
+
+private:
+ static const int MarkerRadius = 2;
+ static const QColor MarkerColor;
+ static const QColor FillColor;
+
+private:
+ ChartScene* scene;
+ QPointF origin;
+ qreal xSpacing;
+};
+
+#endif
diff --git a/src/charts/ChartMarker.cpp b/src/charts/ChartMarker.cpp
new file mode 100644
index 0000000..7fdd8cd
--- /dev/null
+++ b/src/charts/ChartMarker.cpp
@@ -0,0 +1,50 @@
+#include "ChartMarker.h"
+#include "ChartToolTip.h"
+
+const QPointF ChartMarker::ToolTipOffset(45, -35);
+const QColor ChartMarker::Color(Qt::blue);
+
+ChartMarker::ChartMarker(const QPointF& center, const QString& toolTipText):
+ radius(Radius), toolTip(NULL), toolTipText(toolTipText)
+{
+ setPos(center);
+ setRect(QRectF(-HoverRadius, -HoverRadius,
+ 2 * HoverRadius, 2 * HoverRadius));
+ setAcceptHoverEvents(true);
+}
+
+ChartMarker::~ChartMarker()
+{
+ delete toolTip;
+}
+
+void ChartMarker::hoverEnterEvent(QGraphicsSceneHoverEvent*)
+{
+ radius = HighlightedRadius;
+ update();
+ if(!toolTip)
+ createToolTip();
+ toolTip->show();
+}
+
+void ChartMarker::createToolTip()
+{
+ toolTip = new ChartToolTip(mapToScene(ToolTipOffset), toolTipText);
+ scene()->addItem(toolTip);
+ toolTip->adjustPos();
+}
+
+void ChartMarker::hoverLeaveEvent(QGraphicsSceneHoverEvent*)
+{
+ radius = Radius;
+ update();
+ toolTip->hide();
+}
+
+void ChartMarker::paint(QPainter* painter, const QStyleOptionGraphicsItem*,
+ QWidget*)
+{
+ painter->setPen(Color);
+ painter->setBrush(Color);
+ painter->drawEllipse(QPointF(), radius, radius);
+}
diff --git a/src/charts/ChartMarker.h b/src/charts/ChartMarker.h
new file mode 100644
index 0000000..1f3d81e
--- /dev/null
+++ b/src/charts/ChartMarker.h
@@ -0,0 +1,36 @@
+#ifndef CHART_MARKER_H
+#define CHART_MARKER_H
+
+#include <QtWidgets>
+
+class ChartToolTip;
+
+class ChartMarker: public QGraphicsEllipseItem
+{
+public:
+ ChartMarker(const QPointF& center, const QString& toolTipText);
+ ~ChartMarker();
+ void paint(QPainter* painter,
+ const QStyleOptionGraphicsItem* option, QWidget* widget = 0);
+
+protected:
+ void hoverEnterEvent(QGraphicsSceneHoverEvent* event);
+ void hoverLeaveEvent(QGraphicsSceneHoverEvent* event);
+
+private:
+ void createToolTip();
+
+private:
+ static const QColor Color;
+ static const int HoverRadius = 15;
+ static const int Radius = 2;
+ static const int HighlightedRadius = 4;
+ static const QPointF ToolTipOffset;
+
+private:
+ int radius;
+ ChartToolTip* toolTip;
+ QString toolTipText;
+};
+
+#endif
diff --git a/src/charts/ChartScene.cpp b/src/charts/ChartScene.cpp
new file mode 100644
index 0000000..559ecc5
--- /dev/null
+++ b/src/charts/ChartScene.cpp
@@ -0,0 +1,111 @@
+#include "ChartScene.h"
+#include "ChartAxes.h"
+#include <cstdlib>
+#include <cmath>
+#include "ChartDataLine.h"
+
+const QSizeF ChartScene::Margin(30, 20);
+const QSizeF ChartScene::Padding(20, 20);
+const QSizeF ChartScene::DataSetPadding(10, 10);
+
+ChartScene::ChartScene(Chart* chart):
+ QGraphicsScene(chart), chart(chart), dataDirection(1)
+{
+ setSceneRect(0, 0, 600, 300);
+}
+
+void ChartScene::setDataSet(const QList<DataPoint>& dataSet, int xTickInterval)
+{
+ clear();
+ this->dataSet = dataSet;
+ initYScale();
+ ChartAxes(this, xTickInterval).paint();
+ ChartDataLine(this).paint();
+}
+
+void ChartScene::initYScale()
+{
+ yMin = computeMinY();
+ yMax = computeMaxY();
+ yTickSpacing = computeYTickSpacing();
+ extendYMinMax();
+ yScale = getDataSetSize().height() / getYDiff();
+}
+
+int ChartScene::computeMinY() const
+{
+ int min = 1000000;
+ foreach(DataPoint point, dataSet)
+ if(point.value < min)
+ min = point.value;
+ return min;
+}
+
+int ChartScene::computeMaxY() const
+{
+ int max = -1000000;
+ foreach(DataPoint point, dataSet)
+ if(point.value > max)
+ max = point.value;
+ return max;
+}
+
+int ChartScene::computeYTickSpacing() const
+{
+ const int higherZonesLimit = 4;
+ const int lowerZonesLimit = 3;
+
+ double order = floor(log10(getYDiff()));
+ double base = pow(10, order);
+ double zonesNum = ceil(getYDiff() / base);
+ if(zonesNum > higherZonesLimit)
+ base *= 2;
+ else if(zonesNum < lowerZonesLimit)
+ base /= 2;
+ int res = (int)base;
+ if(res == 0)
+ res = 10;
+ return res;
+}
+
+void ChartScene::extendYMinMax()
+{
+ yMin = int(floor(yMin / double(yTickSpacing))) * yTickSpacing;
+ yMax = int(ceil(yMax / double(yTickSpacing))) * yTickSpacing;
+ if (yMax == yMin)
+ yMax = yMin + yTickSpacing;
+}
+
+QSizeF ChartScene::getBorder()
+{
+ return Margin + Padding;
+}
+
+QRectF ChartScene::getChartRect() const
+{
+ QSizeF border = getBorder();
+ return sceneRect().adjusted(border.width(), border.height(),
+ -border.width(), -border.height());
+}
+
+QPointF ChartScene::getChartOrigin() const
+{
+ QRectF chartRect = getChartRect();
+ return QPointF(chartRect.left(), chartRect.bottom());
+}
+
+QSizeF ChartScene::getDataSetSize() const
+{
+ QSizeF dataSetBorder(2 * getBorder() + DataSetPadding);
+ return sceneRect().size() - dataSetBorder;
+}
+
+qreal ChartScene::getXTickSpacing() const
+{
+ return getDataSetSize().width() / (dataSet.size() - 1);
+}
+
+qreal ChartScene::getYPosFromValue(int value) const
+{
+ return yScale * value;
+}
diff --git a/src/charts/ChartScene.h b/src/charts/ChartScene.h
new file mode 100644
index 0000000..3a9af62
--- /dev/null
+++ b/src/charts/ChartScene.h
@@ -0,0 +1,55 @@
+#ifndef CHART_SCENE_H
+#define CHART_SCENE_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "Chart.h"
+#include "DataPoint.h"
+
+class ChartScene: public QGraphicsScene
+{
+public:
+ static const QSizeF Margin;
+ static const QSizeF Padding;
+ static const QSizeF DataSetPadding;
+
+public:
+ static QSizeF getBorder();
+
+public:
+ ChartScene(Chart* chart);
+ void setDataSet(const QList<DataPoint>& dataSet, int xTickInterval);
+ void setDataDirection(int direction) { dataDirection = direction; }
+ QString getXLabel() const { return chart->getXLabel(); }
+ QString getYLabel() const { return chart->getYLabel(); }
+ int getDataDirection() const { return dataDirection; }
+ QPointF getChartOrigin() const;
+ QRectF getChartRect() const;
+ QSizeF getDataSetSize() const;
+ QList<DataPoint> getDataSet() const {return dataSet; }
+ qreal getXTickSpacing() const;
+ qreal getYPosFromValue(int value) const;
+ int getMinY() const { return yMin; }
+ int getMaxY() const { return yMax; }
+ int getYDiff() const { return yMax - yMin; }
+ int getYTickSpacing() const { return yTickSpacing; }
+
+private:
+ void initYScale();
+ int computeMinY() const;
+ int computeMaxY() const;
+ int computeYTickSpacing() const;
+ void extendYMinMax();
+
+private:
+ Chart* chart;
+ QList<DataPoint> dataSet;
+ int yMin;
+ int yMax;
+ qreal yScale;
+ int yTickSpacing;
+ int dataDirection;
+};
+
+#endif
diff --git a/src/charts/ChartToolTip.cpp b/src/charts/ChartToolTip.cpp
new file mode 100644
index 0000000..15783ea
--- /dev/null
+++ b/src/charts/ChartToolTip.cpp
@@ -0,0 +1,43 @@
+#include "ChartToolTip.h"
+
+const QFont ChartToolTip::Font("Sans Serif", 11);
+
+ChartToolTip::ChartToolTip(const QPointF& pos, const QString& text):
+ text(text)
+{
+ setPos(pos);
+ document.setDefaultFont(Font);
+ document.setHtml(text);
+ QSizeF docSize = document.size();
+ setRect(QRectF(QPointF(-docSize.width() / 2, -docSize.height() / 2), docSize));
+}
+
+void ChartToolTip::paint(QPainter* painter, const QStyleOptionGraphicsItem*,
+ QWidget*)
+{
+ drawFrame(painter);
+ drawText(painter);
+}
+
+void ChartToolTip::drawFrame(QPainter* painter)
+{
+ painter->setPen(QPen(Qt::black));
+ painter->setBrush(QBrush(Qt::white));
+ painter->drawRect(rect());
+}
+
+void ChartToolTip::drawText(QPainter* painter)
+{
+ painter->translate(rect().topLeft());
+ document.drawContents(painter);
+}
+
+void ChartToolTip::adjustPos()
+{
+ QRectF toolTipRect = mapRectToScene(boundingRect());
+ QRectF sceneRect = scene()->sceneRect();
+ if(toolTipRect.right() >= sceneRect.right())
+ moveBy(-(toolTipRect.right() - sceneRect.right()) - 1, 0);
+ if(toolTipRect.top() <= sceneRect.top())
+ moveBy(0, sceneRect.top() - toolTipRect.top() + 1);
+}
diff --git a/src/charts/ChartToolTip.h b/src/charts/ChartToolTip.h
new file mode 100644
index 0000000..88453a8
--- /dev/null
+++ b/src/charts/ChartToolTip.h
@@ -0,0 +1,26 @@
+#ifndef CHART_TOOL_TIP_H
+#define CHART_TOOL_TIP_H
+
+#include <QtWidgets>
+
+class ChartToolTip: public QGraphicsRectItem
+{
+public:
+ ChartToolTip(const QPointF& pos, const QString& text);
+ void paint(QPainter* painter,
+ const QStyleOptionGraphicsItem* option, QWidget* widget = 0);
+ void adjustPos();
+
+private:
+ void drawFrame(QPainter* painter);
+ void drawText(QPainter* painter);
+
+private:
+ static const QFont Font;
+
+private:
+ QString text;
+ QTextDocument document;
+};
+
+#endif
diff --git a/src/charts/ChartView.cpp b/src/charts/ChartView.cpp
new file mode 100644
index 0000000..24a08c5
--- /dev/null
+++ b/src/charts/ChartView.cpp
@@ -0,0 +1,12 @@
+#include "ChartView.h"
+
+ChartView::ChartView(QGraphicsScene* scene):
+ QGraphicsView(scene)
+{
+ setRenderHints(QPainter::Antialiasing);
+}
+
+void ChartView::resizeEvent(QResizeEvent*)
+{
+ fitInView(scene()->sceneRect(), Qt::KeepAspectRatio);
+}
diff --git a/src/charts/ChartView.h b/src/charts/ChartView.h
new file mode 100644
index 0000000..fcd071c
--- /dev/null
+++ b/src/charts/ChartView.h
@@ -0,0 +1,16 @@
+#ifndef CHART_VIEW_H
+#define CHART_VIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class ChartView: public QGraphicsView
+{
+public:
+ ChartView(QGraphicsScene* scene);
+
+protected:
+ void resizeEvent(QResizeEvent* event);
+};
+
+#endif
diff --git a/src/charts/DataPoint.h b/src/charts/DataPoint.h
new file mode 100644
index 0000000..394d1be
--- /dev/null
+++ b/src/charts/DataPoint.h
@@ -0,0 +1,15 @@
+#ifndef DATAPOINT_H
+#define DATAPOINT_H
+
+struct DataPoint
+{
+public:
+ DataPoint(const QString& label, int value, QString toolTipLabel = ""):
+ label(label), value(value), toolTipLabel(toolTipLabel) {}
+
+ QString label;
+ int value;
+ QString toolTipLabel;
+};
+
+#endif
diff --git a/src/charts/PieChart.cpp b/src/charts/PieChart.cpp
new file mode 100644
index 0000000..0637823
--- /dev/null
+++ b/src/charts/PieChart.cpp
@@ -0,0 +1,28 @@
+#include "PieChart.h"
+#include "PieChartScene.h"
+#include "ChartView.h"
+
+PieChart::PieChart():
+ scene(new PieChartScene(this))
+{
+ createChartView();
+}
+
+void PieChart::createChartView()
+{
+ view = new ChartView(scene);
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(view);
+ mainLt->setContentsMargins(QMargins());
+ setLayout(mainLt);
+}
+
+void PieChart::setDataSet(const QList<DataPoint>& dataSet)
+{
+ scene->setDataSet(dataSet);
+}
+
+void PieChart::setColors(const QStringList& colors)
+{
+ scene->setColors(colors);
+}
diff --git a/src/charts/PieChart.h b/src/charts/PieChart.h
new file mode 100644
index 0000000..8e60833
--- /dev/null
+++ b/src/charts/PieChart.h
@@ -0,0 +1,27 @@
+#ifndef PIE_CHART_H
+#define PIE_CHART_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class ChartView;
+class PieChartScene;
+
+class PieChart: public QWidget
+{
+public:
+ PieChart();
+ void setDataSet(const QList<DataPoint>& dataSet);
+ void setColors(const QStringList& colors);
+
+private:
+ void createChartView();
+
+protected:
+ PieChartScene* scene;
+ ChartView* view;
+};
+
+#endif
diff --git a/src/charts/PieChartScene.cpp b/src/charts/PieChartScene.cpp
new file mode 100644
index 0000000..135a0e4
--- /dev/null
+++ b/src/charts/PieChartScene.cpp
@@ -0,0 +1,30 @@
+#include <cstdlib>
+#include <cmath>
+#include "PieChartScene.h"
+#include "PieRound.h"
+#include "PieLegend.h"
+
+const QSizeF PieChartScene::Margin(20, 20);
+const QSizeF PieChartScene::Size(500, 230);
+
+PieChartScene::PieChartScene(PieChart* chart):
+ QGraphicsScene(chart), chart(chart)
+{
+ setSceneRect(0, 0, Size.width(), Size.height());
+}
+
+void PieChartScene::setDataSet(const QList<DataPoint>& dataSet)
+{
+ clear();
+ this->dataSet = dataSet;
+ int centerPos = PieRound::Margin + PieRound::Radius;
+ QPointF roundPos(QPointF(Margin.width(), Margin.height()) +
+ QPointF(centerPos, centerPos));
+ addItem(new PieRound(roundPos, this));
+ addItem(new PieLegend(roundPos + QPointF(LegendDistance, 0), this));
+}
+
+void PieChartScene::setColors(const QStringList& colors)
+{
+ this->colors = colors;
+}
diff --git a/src/charts/PieChartScene.h b/src/charts/PieChartScene.h
new file mode 100644
index 0000000..4ab8129
--- /dev/null
+++ b/src/charts/PieChartScene.h
@@ -0,0 +1,30 @@
+#ifndef PIE_CHART_SCENE_H
+#define PIE_CHART_SCENE_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "DataPoint.h"
+#include "PieChart.h"
+
+class PieChartScene: public QGraphicsScene
+{
+public:
+ static const QSizeF Margin;
+ static const QSizeF Size;
+ static const int LegendDistance = 230;
+
+public:
+ PieChartScene(PieChart* chart);
+ void setDataSet(const QList<DataPoint>& dataSet);
+ void setColors(const QStringList& colors);
+ QList<DataPoint> getDataSet() const {return dataSet; }
+ QStringList getColors() const { return colors; }
+
+private:
+ PieChart* chart;
+ QList<DataPoint> dataSet;
+ QStringList colors;
+};
+
+#endif
diff --git a/src/charts/PieLegend.cpp b/src/charts/PieLegend.cpp
new file mode 100644
index 0000000..5faabf4
--- /dev/null
+++ b/src/charts/PieLegend.cpp
@@ -0,0 +1,48 @@
+#include "PieLegend.h"
+#include "PieChartScene.h"
+
+PieLegend::PieLegend(const QPointF& pos, const PieChartScene* scene):
+ scene(scene)
+{
+ setPos(pos);
+ addLabels();
+}
+
+QRectF PieLegend::boundingRect() const
+{
+ return QRectF(-Width / 2, -Width / 2, Width, Width);
+}
+
+void PieLegend::addLabels()
+{
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ addLabel(i, getLabelPos(i));
+}
+
+QPointF PieLegend::getLabelPos(int index) const
+{
+ return boundingRect().topLeft() +
+ QPointF(SquareSide / 2, SquareSide / 2) +
+ index * QPointF(0, SquareSide + LabelSpacing);
+}
+
+void PieLegend::addLabel(int index, const QPointF& pos)
+{
+ QRectF squareRect(-SquareSide / 2., -SquareSide / 2.,
+ SquareSide, SquareSide);
+ QGraphicsRectItem* squareItem = new QGraphicsRectItem(squareRect, this);
+ squareItem->setBrush(QColor(scene->getColors()[index]));
+ squareItem->setPos(pos);
+
+ QGraphicsSimpleTextItem* textItem =
+ new QGraphicsSimpleTextItem(scene->getDataSet()[index].label, this);
+ textItem->setPos(getTextPos(pos, textItem));
+}
+
+QPointF PieLegend::getTextPos(const QPointF& squarePos,
+ const QGraphicsSimpleTextItem* textItem) const
+{
+ return squarePos +
+ QPointF(SquareSide / 2. + LabelTextSpacing,
+ -textItem->boundingRect().height() / 2);
+}
diff --git a/src/charts/PieLegend.h b/src/charts/PieLegend.h
new file mode 100644
index 0000000..d8e83e5
--- /dev/null
+++ b/src/charts/PieLegend.h
@@ -0,0 +1,34 @@
+#ifndef PIE_LEGEND_H
+#define PIE_LEGEND_H
+
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class PieChartScene;
+
+class PieLegend: public QGraphicsItem
+{
+public:
+ static const int Width = 150;
+ static const int SquareSide = 20;
+ static const int LabelSpacing = 10;
+ static const int LabelTextSpacing = 10;
+
+public:
+ PieLegend(const QPointF& pos, const PieChartScene* scene);
+ QRectF boundingRect() const;
+ void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget* = 0) {}
+
+private:
+ void addLabels();
+ QPointF getLabelPos(int index) const;
+ void addLabel(int index, const QPointF& pos);
+ QPointF getTextPos(const QPointF& squarePos,
+ const QGraphicsSimpleTextItem* textItem) const;
+
+private:
+ const PieChartScene* scene;
+};
+
+#endif
diff --git a/src/charts/PieRound.cpp b/src/charts/PieRound.cpp
new file mode 100644
index 0000000..f90d740
--- /dev/null
+++ b/src/charts/PieRound.cpp
@@ -0,0 +1,77 @@
+#include "PieRound.h"
+#include "PieChartScene.h"
+
+PieRound::PieRound(const QPointF& center, const PieChartScene* scene):
+ scene(scene)
+{
+ setPos(center);
+ calculateSum();
+ addSectors();
+}
+
+QRectF PieRound::boundingRect() const
+{
+ return QRectF(-Radius, -Radius, 2 * Radius, 2 * Radius);
+}
+
+void PieRound::calculateSum()
+{
+ sum = 0;
+ foreach(DataPoint point, scene->getDataSet())
+ sum += point.value;
+}
+
+void PieRound::addSectors()
+{
+ qreal angle = -90;
+ for(int i = 0; i < scene->getDataSet().size(); i++)
+ {
+ qreal sweep = 360. * scene->getDataSet()[i].value / sum;
+ addSector(i, angle, sweep);
+ angle += sweep;
+ }
+}
+
+void PieRound::addSector(int index, qreal startAngle, qreal sweep)
+{
+ addSectorItem(createSectorPath(startAngle, sweep),
+ QColor(scene->getColors()[index]));
+ addSectorLabel(index, startAngle, sweep);
+}
+
+QPainterPath PieRound::createSectorPath(qreal startAngle, qreal sweep)
+{
+ QPainterPath sectorPath;
+ sectorPath.arcTo(boundingRect(), -startAngle, -sweep);
+ sectorPath.lineTo(QPointF());
+ return sectorPath;
+}
+
+void PieRound::addSectorItem(const QPainterPath& path, QColor color)
+{
+ QGraphicsPathItem* sectorItem = new QGraphicsPathItem(path, this);
+ sectorItem->setPen(QColor("white"));
+ sectorItem->setBrush(color);
+}
+
+void PieRound::addSectorLabel(int index, qreal startAngle, qreal sweep)
+{
+ int value = scene->getDataSet()[index].value;
+ if(value == 0)
+ return;
+ QGraphicsSimpleTextItem* labelItem =
+ new QGraphicsSimpleTextItem(QString::number(value), this);
+ labelItem->setPos(getLabelPos(startAngle, sweep, labelItem));
+}
+
+QPointF PieRound::getLabelPos(qreal startAngle, qreal sweep,
+ QGraphicsSimpleTextItem* labelItem)
+{
+ int radius = sweep > 30 ? LabelRadius : 2 * LabelRadius;
+ QRectF labelArcRect(-radius, -radius, 2 * radius, 2 * radius);
+ QPainterPath path;
+ path.arcMoveTo(labelArcRect, -startAngle - sweep / 2);
+ QSizeF labelSize = labelItem->boundingRect().size();
+ return path.currentPosition() -
+ QPointF(labelSize.width() / 2, labelSize.height() / 2);
+}
diff --git a/src/charts/PieRound.h b/src/charts/PieRound.h
new file mode 100644
index 0000000..1963f26
--- /dev/null
+++ b/src/charts/PieRound.h
@@ -0,0 +1,37 @@
+#ifndef PIE_ROUND_H
+#define PIE_ROUND_H
+
+#include <QtWidgets>
+
+#include "DataPoint.h"
+
+class PieChartScene;
+
+class PieRound: public QGraphicsItem
+{
+public:
+ static const int Margin = 20;
+ static const int Radius = 80;
+ static const int LabelRadius = 50;
+
+public:
+ PieRound(const QPointF& center, const PieChartScene* scene);
+ QRectF boundingRect() const;
+ void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget* = 0) {}
+
+private:
+ void calculateSum();
+ void addSectors();
+ void addSector(int index, qreal startAngle, qreal sweep);
+ QPainterPath createSectorPath(qreal startAngle, qreal sweep);
+ void addSectorItem(const QPainterPath& path, QColor color);
+ void addSectorLabel(int index, qreal startAngle, qreal sweep);
+ QPointF getLabelPos(qreal startAngle, qreal sweep,
+ QGraphicsSimpleTextItem* labelItem);
+
+private:
+ int sum;
+ const PieChartScene* scene;
+};
+
+#endif
diff --git a/src/charts/TimeChart.cpp b/src/charts/TimeChart.cpp
new file mode 100644
index 0000000..c6a60b9
--- /dev/null
+++ b/src/charts/TimeChart.cpp
@@ -0,0 +1,155 @@
+#include "TimeChart.h"
+#include "ChartScene.h"
+#include "DataPoint.h"
+
+const QList<int> TimeChart::TimeUnits = QList<int>() <<
+ 1 << 7 << 30 << 92 << 183;
+
+TimeChart::TimeChart()
+{
+}
+
+void TimeChart::setDates(const QList<QDateTime>& dates, int period,
+ int dataDirection)
+{
+ this->dates = dates;
+ scene->setDataDirection(dataDirection);
+ TimeUnit timeUnit = findTimeUnit(period);
+ QDate thisIntervalStart = getIntervalStart(QDate::currentDate(), timeUnit);
+ QList<DataPoint> dataSet;
+ int pointsNum = getDataPointsNum(period, timeUnit);
+ for(int i = 0; i < pointsNum; i++)
+ dataSet.append(createDataPoint(thisIntervalStart, timeUnit, i));
+ scene->setDataSet(dataSet, getTicksInterval(pointsNum));
+}
+
+int TimeChart::getDataPointsNum(int period, TimeUnit timeUnit)
+{
+ return period / TimeUnits.at(timeUnit);
+}
+
+int TimeChart::getTicksInterval(int pointsNum)
+{
+ int ticksInterval = 1;
+ while(pointsNum / ticksInterval > MaxXTicks)
+ ticksInterval *= 2;
+ return ticksInterval;
+}
+
+TimeChart::TimeUnit TimeChart::findTimeUnit(int period)
+{
+ for(int unit = Day; unit < (int)TimeUnitsNum; unit++)
+ if(getDataPointsNum(period, (TimeUnit)unit) <= MaxDataPoints)
+ return (TimeUnit)unit;
+ return HalfYear;
+}
+
+QDate TimeChart::getIntervalStart(const QDate& date, TimeUnit timeUnit)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ return date;
+ case Week:
+ return date.addDays(-date.dayOfWeek() + 1);
+ case Month:
+ return QDate(date.year(), date.month(), 1);
+ case Quarter:
+ return getQuarterStart(date);
+ case HalfYear:
+ return getHalfYearStart(date);
+ default:
+ return date;
+ }
+}
+
+QDate TimeChart::getQuarterStart(const QDate& date)
+{
+ const int quarterLen = 3;
+ int quarter = (date.month() - 1) / quarterLen;
+ int startMonth = quarter * quarterLen + 1;
+ return QDate(date.year(), startMonth, 1);
+}
+
+QDate TimeChart::getHalfYearStart(const QDate& date)
+{
+ const int secondHalfStart = 7;
+ int startMonth = date.month() < secondHalfStart ? 1 : secondHalfStart;
+ return QDate(date.year(), startMonth, 1);
+}
+
+QDate TimeChart::getInterval(const QDate& start, TimeUnit timeUnit,
+ int num)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ return start.addDays(num);
+ case Week:
+ return start.addDays(7 * num);
+ case Month:
+ return start.addMonths(num);
+ case Quarter:
+ return start.addMonths(3 * num);
+ case HalfYear:
+ return start.addMonths(6 * num);
+ default:
+ return start;
+ }
+}
+
+QDate TimeChart::getIntervalEnd(const QDate& start, TimeUnit timeUnit)
+{
+ return getInterval(start, timeUnit, 1).addDays(-1);
+}
+
+DataPoint TimeChart::createDataPoint(const QDate& thisIntervalStart, TimeUnit timeUnit,
+ int intervalIndex)
+{
+ QDate startDate = getInterval(thisIntervalStart, timeUnit,
+ scene->getDataDirection() * intervalIndex);
+ QDate endDate = getIntervalEnd(startDate, timeUnit);
+ QString xLabel = getDateLabel(startDate, timeUnit);
+ int value = getCardsNumForDate(startDate, endDate);
+ QString toolTipLabel = getIntervalLabel(startDate, endDate, timeUnit);
+ return DataPoint(xLabel, value, toolTipLabel);
+}
+
+QString TimeChart::getDateLabel(const QDate& date, TimeUnit timeUnit)
+{
+ return date.toString(getIntervalFormat(timeUnit));
+}
+
+QString TimeChart::getIntervalFormat(TimeUnit timeUnit)
+{
+ switch(timeUnit)
+ {
+ case Day:
+ case Week:
+ return "dd.MM";
+ case Month:
+ case Quarter:
+ case HalfYear:
+ return "MM/yy";
+ default:
+ return "dd.MM";
+ }
+}
+
+QString TimeChart::getIntervalLabel(const QDate& startDate,
+ const QDate& endDate, TimeUnit timeUnit)
+{
+ QString startLabel = getDateLabel(startDate, timeUnit);
+ if(timeUnit == Day || timeUnit == Month)
+ return startLabel;
+ return startLabel + " - " + getDateLabel(endDate, timeUnit);
+}
+
+int TimeChart::getCardsNumForDate(const QDate& startDate, const QDate& endDate)
+{
+ int cardsNum = 0;
+ foreach(QDateTime date, dates)
+ if(date.date() >= startDate && date.date() <= endDate)
+ cardsNum++;
+ return cardsNum;
+}
diff --git a/src/charts/TimeChart.h b/src/charts/TimeChart.h
new file mode 100644
index 0000000..1d9f92d
--- /dev/null
+++ b/src/charts/TimeChart.h
@@ -0,0 +1,54 @@
+#ifndef TIME_CHART_H
+#define TIME_CHART_H
+
+#include <QtCore>
+
+#include "Chart.h"
+
+class TimeChart: public Chart
+{
+enum TimeUnit
+{
+ Day = 0,
+ Week,
+ Month,
+ Quarter,
+ HalfYear,
+ TimeUnitsNum
+};
+
+public:
+ TimeChart();
+ void setDates(const QList<QDateTime>& dates, int period,
+ int dataDirection = 1);
+
+private:
+ static TimeUnit findTimeUnit(int period);
+ static QDate getIntervalStart(const QDate& date, TimeUnit timeUnit);
+ static QDate getInterval(const QDate& start, TimeUnit timeUnit,
+ int num);
+ static QDate getIntervalEnd(const QDate& start, TimeUnit timeUnit);
+ static QString getDateLabel(const QDate& date, TimeUnit timeUnit);
+ static QString getIntervalFormat(TimeUnit timeUnit);
+ static QString getIntervalLabel(const QDate& startDate,
+ const QDate& endDate, TimeUnit timeUnit);
+ static QDate getQuarterStart(const QDate& date);
+ static QDate getHalfYearStart(const QDate& date);
+ static int getDataPointsNum(int period, TimeUnit timeUnit);
+ static int getTicksInterval(int pointsNum);
+
+private:
+ DataPoint createDataPoint(const QDate& thisIntervalStart, TimeUnit timeUnit,
+ int intervalIndex);
+ int getCardsNumForDate(const QDate& startDate, const QDate& endDate);
+
+private:
+ static const QList<int> TimeUnits;
+ static const int MaxDataPoints = 28;
+ static const int MaxXTicks = 12;
+
+private:
+ QList<QDateTime> dates;
+};
+
+#endif
diff --git a/src/dic-options/DictionaryOptionsDialog.cpp b/src/dic-options/DictionaryOptionsDialog.cpp
new file mode 100644
index 0000000..46bb245
--- /dev/null
+++ b/src/dic-options/DictionaryOptionsDialog.cpp
@@ -0,0 +1,62 @@
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/CardPack.h"
+#include "FieldsPage.h"
+#include "PacksPage.h"
+#include "FieldsListModel.h"
+
+DictionaryOptionsDialog::DictionaryOptionsDialog( Dictionary* aDict, QWidget* aParent ):
+ QDialog( aParent ), m_origDictPtr( aDict )
+ {
+ initData();
+ createPages();
+
+ QDialogButtonBox* okCancelBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
+ Qt::Horizontal );
+ connect( okCancelBox, SIGNAL(accepted()), this, SLOT(accept()) );
+ connect( okCancelBox, SIGNAL(rejected()), this, SLOT(reject()) );
+
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->addWidget(createDictPathLabel());
+ mainLayout->addWidget( m_pages );
+ mainLayout->addWidget( okCancelBox );
+ setLayout( mainLayout );
+
+ setWindowTitle( tr("Dictionary options") );
+ }
+
+DictionaryOptionsDialog::~DictionaryOptionsDialog()
+{
+ delete m_emptyField;
+}
+
+QSize DictionaryOptionsDialog::sizeHint() const
+{
+ return QSize(700, 400);
+}
+
+QLabel* DictionaryOptionsDialog::createDictPathLabel()
+{
+ const int MaxPathLength = 500;
+ QLabel* dictPathLabel = new QLabel;
+ QFontMetrics metrics(dictPathLabel->font());
+ QString dictPath = QDir::toNativeSeparators(m_origDictPtr->getFilePath());
+ dictPath = metrics.elidedText(dictPath, Qt::ElideMiddle, MaxPathLength);
+ dictPathLabel->setText("<b>" + tr("File name") + "</b>: " + dictPath);
+ return dictPathLabel;
+}
+
+void DictionaryOptionsDialog::initData()
+ {
+ m_dict.setDictConfig( m_origDictPtr );
+ m_emptyField = new Field();
+ m_fieldsListModel = new FieldsListModel( this );
+ }
+
+void DictionaryOptionsDialog::createPages()
+ {
+ m_pages = new QTabWidget;
+ m_pages->addTab( new FieldsPage( this ), QIcon(":/images/fields.png"), tr("Fields") );
+ m_pages->addTab( new PacksPage( this ), QIcon(":/images/word-drill.png"), tr("Card packs") );
+ }
+
+
diff --git a/src/dic-options/DictionaryOptionsDialog.h b/src/dic-options/DictionaryOptionsDialog.h
new file mode 100644
index 0000000..3a57cb6
--- /dev/null
+++ b/src/dic-options/DictionaryOptionsDialog.h
@@ -0,0 +1,41 @@
+#ifndef DICTIONARYOPTIONSDIALOG_H
+#define DICTIONARYOPTIONSDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "../dictionary/Field.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Dictionary.h"
+
+class Dictionary;
+class FieldsListModel;
+
+class DictionaryOptionsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ DictionaryOptionsDialog( Dictionary* aDict, QWidget* aParent );
+ ~DictionaryOptionsDialog();
+ QSize sizeHint() const;
+
+private:
+ void initData();
+ void createPages();
+ QLabel* createDictPathLabel();
+
+public:
+ FieldsListModel* m_fieldsListModel;
+ Field* m_emptyField;
+ Dictionary m_dict;
+
+private:
+ Dictionary* m_origDictPtr;
+ QListWidget* m_contentsWidget;
+ QTabWidget* m_pages;
+};
+
+Q_DECLARE_METATYPE(QList<QPersistentModelIndex>)
+
+#endif // DICTIONARYOPTIONSDIALOG_H
diff --git a/src/dic-options/DraggableListModel.cpp b/src/dic-options/DraggableListModel.cpp
new file mode 100644
index 0000000..2ea0523
--- /dev/null
+++ b/src/dic-options/DraggableListModel.cpp
@@ -0,0 +1,83 @@
+#include "DraggableListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+Qt::ItemFlags DraggableListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsDragEnabled | defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+ }
+
+Qt::DropActions DraggableListModel::supportedDropActions() const
+ {
+ return Qt::MoveAction;
+ }
+
+QStringList DraggableListModel::mimeTypes() const
+ {
+ QStringList types;
+ types << "application/octet-stream";
+ return types;
+ }
+
+QMimeData *DraggableListModel::mimeData(const QModelIndexList &indexes) const
+ {
+ QStringList list;
+ QModelIndexList validIndexes;
+ foreach (QModelIndex index, indexes)
+ if (index.isValid() && index.column() == 0)
+ validIndexes << index;
+ qSort(validIndexes);
+
+ int num = validIndexes.size();
+ QByteArray encodedData;
+ QDataStream stream( &encodedData, QIODevice::WriteOnly );
+ const void** ptrs = new const void*[num];
+ int i=0;
+ foreach (QModelIndex index, validIndexes)
+ ptrs[i++] = dataPtr( index );
+ stream.writeBytes( (char*)ptrs, num*sizeof(void*) );
+ delete ptrs;
+ QMimeData *mimeData = new QMimeData();
+ mimeData->setData( "application/octet-stream", encodedData );
+ return mimeData;
+ }
+
+bool DraggableListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int /*column*/, const QModelIndex &parent)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+
+ int beginRow;
+ if (row != -1)
+ beginRow = row;
+ else if (parent.isValid())
+ beginRow = parent.row();
+ else
+ beginRow = rowCount(QModelIndex());
+
+ QByteArray encodedData = data->data("application/octet-stream");
+ QDataStream stream( &encodedData, QIODevice::ReadOnly );
+ void** ptrs = new void*[ rowCount() ];
+ uint num;
+ stream.readBytes( (char*&)ptrs, num ); //TODO: Avoid converting pointer to other types
+ num /= sizeof(void*);
+
+ QList<QPersistentModelIndex> movedIndexes;
+ for(uint i=0; i<num; i++)
+ {
+ insertPointer( beginRow + i, ptrs[i] );
+ movedIndexes << QPersistentModelIndex( index(beginRow + i, 0, QModelIndex()) );
+ }
+ delete ptrs;
+ emit indexesDropped( movedIndexes );
+ return true;
+ }
diff --git a/src/dic-options/DraggableListModel.h b/src/dic-options/DraggableListModel.h
new file mode 100644
index 0000000..e323def
--- /dev/null
+++ b/src/dic-options/DraggableListModel.h
@@ -0,0 +1,33 @@
+#ifndef DRAGGABLELISTMODEL_H
+#define DRAGGABLELISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class DraggableListModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ DraggableListModel( DictionaryOptionsDialog* aParent ):
+ QAbstractListModel( aParent ), m_parent( aParent )
+ {}
+
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ Qt::DropActions supportedDropActions() const;
+ QStringList mimeTypes() const;
+ QMimeData * mimeData(const QModelIndexList &indexes) const;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
+ virtual const void* dataPtr( const QModelIndex &aIndex ) const = 0;
+ virtual void insertPointer(int aPos, void *aData) = 0;
+
+signals:
+ void indexesDropped(QList<QPersistentModelIndex> aIndexes);
+
+protected:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif
diff --git a/src/dic-options/FieldStyleDelegate.cpp b/src/dic-options/FieldStyleDelegate.cpp
new file mode 100644
index 0000000..6efee8c
--- /dev/null
+++ b/src/dic-options/FieldStyleDelegate.cpp
@@ -0,0 +1,35 @@
+#include "FieldStyleDelegate.h"
+#include "../dictionary/Field.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QComboBox>
+
+FieldStyleDelegate::FieldStyleDelegate(QObject *parent) :
+ QStyledItemDelegate(parent)
+ {
+ }
+
+QWidget* FieldStyleDelegate::createEditor(QWidget *parent,
+ const QStyleOptionViewItem &/*option*/, const QModelIndex &/*index*/) const
+ {
+ QComboBox* comboBox = new QComboBox( parent );
+ comboBox->insertItems( 0, FieldStyleFactory::inst()->getStyleNames() );
+ return comboBox;
+ }
+
+void FieldStyleDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
+ {
+ QString value = index.model()->data(index, Qt::EditRole).toString();
+ int cbIndex = FieldStyleFactory::inst()->getStyleNames().indexOf( value );
+ QComboBox* comboBox = static_cast<QComboBox*>(editor);
+ comboBox->setCurrentIndex( cbIndex );
+ }
+
+void FieldStyleDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
+ const QModelIndex &index) const
+ {
+ QComboBox* comboBox = static_cast<QComboBox*>(editor);
+ QString value = comboBox->currentText();
+ model->setData(index, value, Qt::EditRole);
+ }
+
diff --git a/src/dic-options/FieldStyleDelegate.h b/src/dic-options/FieldStyleDelegate.h
new file mode 100644
index 0000000..2d969f5
--- /dev/null
+++ b/src/dic-options/FieldStyleDelegate.h
@@ -0,0 +1,19 @@
+#ifndef FIELDSTYLEDELEGATE_H
+#define FIELDSTYLEDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+class FieldStyleDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ FieldStyleDelegate(QObject *parent = 0);
+ QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const;
+ void setEditorData(QWidget *editor, const QModelIndex &index) const;
+ void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;
+};
+
+
+#endif // FIELDSTYLEDELEGATE_H
diff --git a/src/dic-options/FieldsListModel.cpp b/src/dic-options/FieldsListModel.cpp
new file mode 100644
index 0000000..9fd23ac
--- /dev/null
+++ b/src/dic-options/FieldsListModel.cpp
@@ -0,0 +1,222 @@
+#include "FieldsListModel.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+QVariant FieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ const Field* field = m_parent->m_dict.field( index.row() );
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ switch( index.column() )
+ {
+ case 0:
+ return field->name();
+ case 1:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ case Qt::EditRole:
+ switch( index.column() )
+ {
+ case 0:
+ return field->name();
+ case 1:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ case EStyleRole:
+ return field->style();
+ default:
+ return QVariant();
+ }
+ }
+
+QVariant FieldsListModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+ {
+ if( role != Qt::DisplayRole )
+ return QVariant();
+ if( orientation == Qt::Horizontal )
+ switch( section )
+ {
+ case 0:
+ return tr("Field");
+ case 1:
+ return tr("Style");
+ default:
+ return QVariant();
+ }
+ else
+ return QVariant();
+ }
+
+Qt::ItemFlags FieldsListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
+ if (index.isValid())
+ return Qt::ItemIsEditable | Qt::ItemIsDragEnabled | defaultFlags;
+ else
+ return Qt::ItemIsDropEnabled | defaultFlags;
+ }
+
+bool FieldsListModel::setData(const QModelIndex &index, const QVariant &value, int role)
+ {
+ if( !index.isValid() || role!=Qt::EditRole )
+ return false;
+ switch( index.column() )
+ {
+ case 0:
+ m_parent->m_dict.setFieldName(index.row(), value.toString());
+ emit dataChanged(index, index);
+ return true;
+ case 1:
+ m_parent->m_dict.setFieldStyle(index.row(), value.toString());
+ emit dataChanged(index, index);
+ return true;
+ default:
+ return true;
+ }
+ }
+
+bool FieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.insertField( position, tr("new field") );
+ endInsertRows();
+ return true;
+ }
+
+bool FieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.removeField( position );
+ endRemoveRows();
+ return true;
+ }
+
+void FieldsListModel::removeField(int aPos )
+ {
+ beginRemoveRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.destroyField( aPos );
+ endRemoveRows();
+ }
+
+void FieldsListModel::insertField(int aPos, Field* aField )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.insertField( aPos, aField );
+ endInsertRows();
+ }
+
+Qt::DropActions FieldsListModel::supportedDropActions() const
+ {
+ return Qt::MoveAction;
+ }
+
+QStringList FieldsListModel::mimeTypes() const
+ {
+ QStringList types;
+ types << "application/octet-stream";
+ return types;
+ }
+
+QMimeData *FieldsListModel::mimeData(const QModelIndexList &indexes) const
+ {
+ QStringList list;
+ QModelIndexList validIndexes;
+ foreach (QModelIndex index, indexes)
+ if (index.isValid() && index.column() == 0)
+ validIndexes << index;
+ qSort(validIndexes);
+
+ int num = validIndexes.size();
+ QByteArray encodedData;
+ QDataStream stream( &encodedData, QIODevice::WriteOnly );
+ const Field** fieldPtrs = new const Field*[num];
+ int i=0;
+ foreach (QModelIndex index, validIndexes)
+ fieldPtrs[i++] = m_parent->m_dict.field( index.row() );
+ stream.writeBytes( (char*)fieldPtrs, num*sizeof(Field*) );
+ delete fieldPtrs;
+ QMimeData *mimeData = new QMimeData();
+ mimeData->setData( "application/octet-stream", encodedData );
+ return mimeData;
+ }
+
+bool FieldsListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int row, int column, const QModelIndex &parent)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+ if (column > 0)
+ column = 0;
+
+ int beginRow;
+ if (row != -1)
+ beginRow = row;
+ else if (parent.isValid())
+ beginRow = parent.row();
+ else
+ beginRow = rowCount(QModelIndex());
+
+ QByteArray encodedData = data->data("application/octet-stream");
+ QDataStream stream( &encodedData, QIODevice::ReadOnly );
+ Field** fieldPtrs = new Field*[ m_parent->m_dict.fieldsNum() ];
+ uint num;
+ stream.readBytes( (char*&)fieldPtrs, num ); //TODO: Avoid converting pointer to other types
+ num /= sizeof(Field*);
+
+ QList<QPersistentModelIndex> movedIndexes;
+ for(uint i=0; i<num; i++)
+ {
+ insertField( beginRow + i, fieldPtrs[i] );
+ movedIndexes << QPersistentModelIndex( index(beginRow + i, 0, QModelIndex()) );
+ }
+ delete fieldPtrs;
+ emit indexesDropped( movedIndexes );
+ return true;
+ }
+
+void FieldsListModel::moveIndexesUpDown( QModelIndexList aIndexes, int aDirection )
+ {
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, aIndexes)
+ if( idx.isValid() && idx.column() == 0 )
+ pSrcIndexes << QPersistentModelIndex( idx );
+
+ int rowsNum = pSrcIndexes.size();
+ if( rowsNum == 0 )
+ return;
+
+ qSort( pSrcIndexes );
+
+ int beginRow;
+ if( aDirection < 0 )
+ beginRow = pSrcIndexes[0].row()-1;
+ else
+ beginRow = pSrcIndexes[rowsNum-1].row()+2;
+ if( rowsNum == 1 && (beginRow < 0 || beginRow > rowCount()) )
+ return;
+ if( beginRow < 0 )
+ beginRow = 0;
+ if( beginRow > rowCount() )
+ beginRow = rowCount();
+
+ QMimeData* mime = mimeData( aIndexes );
+ dropMimeData( mime, Qt::MoveAction, beginRow, 0, QModelIndex() );
+ foreach (QModelIndex index, pSrcIndexes)
+ removeRow( index.row() );
+ }
diff --git a/src/dic-options/FieldsListModel.h b/src/dic-options/FieldsListModel.h
new file mode 100644
index 0000000..1cc4534
--- /dev/null
+++ b/src/dic-options/FieldsListModel.h
@@ -0,0 +1,49 @@
+#ifndef FIELDSLISTMODEL_H
+#define FIELDSLISTMODEL_H
+
+#include <QAbstractTableModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class FieldsListModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum
+ {
+ EStyleRole = Qt::UserRole
+ };
+
+ FieldsListModel( DictionaryOptionsDialog* aParent ):
+ QAbstractTableModel( aParent ), m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.fieldsNum(); }
+ int columnCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return 2; }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
+ bool insertRows(int position, int rows, const QModelIndex &/*parent*/);
+ bool removeRows(int position, int rows, const QModelIndex &/*parent*/);
+ void insertField(int aPos, Field *aField);
+ void removeField( int aPos );
+ Qt::DropActions supportedDropActions() const;
+ QStringList mimeTypes() const;
+ QMimeData* mimeData(const QModelIndexList &indexes) const;
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent);
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ void moveIndexesUpDown(QModelIndexList aIndexes, int aDirection);
+
+signals:
+ void indexesDropped(QList<QPersistentModelIndex> aIndexes);
+
+private:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif // FIELDSLISTMODEL_H
diff --git a/src/dic-options/FieldsPage.cpp b/src/dic-options/FieldsPage.cpp
new file mode 100644
index 0000000..ea9a741
--- /dev/null
+++ b/src/dic-options/FieldsPage.cpp
@@ -0,0 +1,139 @@
+#include "FieldsPage.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/CardPack.h"
+#include "FieldsListModel.h"
+#include "FieldsPreviewModel.h"
+
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <QToolButton>
+#include <QGroupBox>
+
+FieldsPage::FieldsPage( DictionaryOptionsDialog* aParent ):
+ QWidget( aParent ), m_parent( aParent )
+ {
+ createFieldsList();
+ createFieldsPreview();
+
+ QHBoxLayout* mainLt = new QHBoxLayout;
+ mainLt->addLayout( m_fieldsLt );
+ mainLt->addWidget( m_previewGroup );
+ setLayout( mainLt );
+ }
+
+void FieldsPage::createFieldsList()
+ {
+ QGroupBox* fieldsGroup = new QGroupBox( tr("Fields") );
+
+ QToolButton* moveUpBtn = new QToolButton;
+ moveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ moveUpBtn->setObjectName("up");
+ moveUpBtn->setToolTip(tr("Move up"));
+ QToolButton* moveDownBtn = new QToolButton;
+ moveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ moveDownBtn->setObjectName("down");
+ moveDownBtn->setToolTip(tr("Move down"));
+ QVBoxLayout* upDownLt = new QVBoxLayout;
+ upDownLt->addWidget( moveUpBtn );
+ upDownLt->addWidget( moveDownBtn );
+ upDownLt->addStretch( 1 );
+
+ connect( moveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( moveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_fieldsListView = new FieldsView;
+ m_fieldsListView->setModel( m_parent->m_fieldsListModel );
+ QHBoxLayout* listHLt = new QHBoxLayout;
+ listHLt->addWidget( m_fieldsListView );
+ listHLt->addLayout( upDownLt );
+ fieldsGroup->setLayout( listHLt );
+
+ QPushButton* addBtn = new QPushButton( QPixmap(":/images/add.png"), tr("Add") );
+ addBtn->setToolTip(tr("Add new field"));
+ QPushButton* removeBtn = new QPushButton( QPixmap(":/images/delete.png"), tr("Remove") );
+ removeBtn->setToolTip(tr("Remove field(s)"));
+ QPushButton* editBtn = new QPushButton( QPixmap(":/images/edit.png"), tr("Rename") );
+ editBtn->setToolTip(tr("Rename field"));
+ QHBoxLayout* btnLt = new QHBoxLayout;
+ btnLt->addWidget( addBtn );
+ btnLt->addWidget( removeBtn );
+ btnLt->addWidget( editBtn );
+
+ connect( addBtn, SIGNAL(clicked()), this, SLOT(addRow()) );
+ connect( removeBtn, SIGNAL(clicked()), this, SLOT(removeRows()) );
+ connect( editBtn, SIGNAL(clicked()), this, SLOT(editField()) );
+
+ m_fieldsLt = new QVBoxLayout;
+ m_fieldsLt->addWidget( fieldsGroup );
+ m_fieldsLt->addLayout( btnLt );
+ }
+
+void FieldsPage::createFieldsPreview()
+ {
+ FieldsPreviewModel* fieldsPreviewModel = new FieldsPreviewModel( m_parent );
+ m_fieldsPreview = new QListView;
+ m_fieldsPreview->setModel( fieldsPreviewModel );
+
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ m_fieldsPreview, SLOT(reset()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_fieldsPreview, SLOT(reset()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_fieldsPreview, SLOT(reset()) );
+
+ QHBoxLayout* previewLt = new QHBoxLayout;
+ previewLt->addWidget( m_fieldsPreview );
+ m_previewGroup = new QGroupBox(tr("Preview"));
+ m_previewGroup->setLayout( previewLt );
+ }
+
+void FieldsPage::moveItemsUpDown()
+ {
+ int direction;
+ if( sender()->objectName() == "up" )
+ direction = -1;
+ else
+ direction = 1;
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedIndexes();
+ m_fieldsListView->model()->moveIndexesUpDown( selectedIndexes, direction );
+ }
+
+void FieldsPage::addRow()
+ {
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedRows();
+ int row;
+ if( selectedIndexes.size() > 0 )
+ {
+ qSort(selectedIndexes);
+ row = selectedIndexes[selectedIndexes.size()-1].row() + 1;
+ }
+ else
+ row = 0;
+ m_fieldsListView->model()->insertRow( row, QModelIndex() );
+ }
+
+void FieldsPage::removeRows()
+ {
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedRows();
+ QList<QPersistentModelIndex> pIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pIndexes << QPersistentModelIndex( idx );
+ if( pIndexes.size() == 0 )
+ return;
+ foreach( QModelIndex idx, pIndexes )
+ m_fieldsListView->model()->removeField( idx.row() );
+ }
+
+void FieldsPage::editField()
+ {
+ QModelIndex idx = m_fieldsListView->currentIndex();
+ if( !idx.isValid() )
+ return;
+ m_fieldsListView->edit( idx );
+ }
diff --git a/src/dic-options/FieldsPage.h b/src/dic-options/FieldsPage.h
new file mode 100644
index 0000000..3cfdf99
--- /dev/null
+++ b/src/dic-options/FieldsPage.h
@@ -0,0 +1,37 @@
+#ifndef FIELDSPAGE_H
+#define FIELDSPAGE_H
+
+#include <QListView>
+
+#include "DictionaryOptionsDialog.h"
+#include "FieldsView.h"
+
+class Dictionary;
+class QGroupBox;
+class QVBoxLayout;
+
+class FieldsPage : public QWidget
+{
+ Q_OBJECT
+public:
+ FieldsPage( DictionaryOptionsDialog* aParent );
+
+private slots:
+ void moveItemsUpDown();
+ void addRow();
+ void removeRows();
+ void editField();
+
+private:
+ void createFieldsList();
+ void createFieldsPreview();
+
+private:
+ DictionaryOptionsDialog* m_parent;
+ QVBoxLayout* m_fieldsLt;
+ QGroupBox* m_previewGroup;
+ FieldsView* m_fieldsListView;
+ QListView* m_fieldsPreview;
+};
+
+#endif
diff --git a/src/dic-options/FieldsPreviewModel.cpp b/src/dic-options/FieldsPreviewModel.cpp
new file mode 100644
index 0000000..e6ce1c6
--- /dev/null
+++ b/src/dic-options/FieldsPreviewModel.cpp
@@ -0,0 +1,34 @@
+#include "FieldsPreviewModel.h"
+#include "../dictionary/Field.h"
+#include "../field-styles/FieldStyle.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QBrush>
+
+QVariant FieldsPreviewModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ Field* field = m_parent->m_dict.fields().value(index.row());
+ const FieldStyle fieldStyle = FieldStyleFactory::inst()->getStyle(field->style());
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return fieldStyle.prefix + field->name() + fieldStyle.suffix;
+ case Qt::FontRole:
+ return fieldStyle.font;
+ case Qt::BackgroundRole:
+ return QBrush( FieldStyleFactory::inst()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.color;
+ case Qt::SizeHintRole:
+ return QSize(0, 50);
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ }
diff --git a/src/dic-options/FieldsPreviewModel.h b/src/dic-options/FieldsPreviewModel.h
new file mode 100644
index 0000000..38e1fe2
--- /dev/null
+++ b/src/dic-options/FieldsPreviewModel.h
@@ -0,0 +1,27 @@
+#ifndef FIELDSPREVIEWMODEL_H
+#define FIELDSPREVIEWMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "../dictionary/Dictionary.h"
+
+class FieldsPreviewModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ FieldsPreviewModel( DictionaryOptionsDialog* aParent ):
+ QAbstractListModel( aParent ), m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.fieldsNum(); }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ Qt::ItemFlags flags(const QModelIndex &/*index*/) const {return Qt::NoItemFlags;}
+
+private:
+ DictionaryOptionsDialog* m_parent;
+};
+
+#endif // FIELDSPREVIEWMODEL_H
diff --git a/src/dic-options/FieldsView.cpp b/src/dic-options/FieldsView.cpp
new file mode 100644
index 0000000..65801c7
--- /dev/null
+++ b/src/dic-options/FieldsView.cpp
@@ -0,0 +1,41 @@
+#include "FieldsView.h"
+#include "FieldStyleDelegate.h"
+
+#include <QHeaderView>
+
+FieldsView::FieldsView(QWidget *parent):
+ QTableView( parent )
+ {
+ setDragEnabled(true);
+ setAcceptDrops(true);
+ setDropIndicatorShown(true);
+ setDragDropOverwriteMode( false );
+ FieldStyleDelegate* delegate = new FieldStyleDelegate(this);
+ setItemDelegateForColumn( 1, delegate );
+
+ setShowGrid(false);
+ verticalHeader()->hide();
+ setSelectionBehavior( QAbstractItemView::SelectRows );
+ }
+
+void FieldsView::startDrag(Qt::DropActions supportedActions)
+ {
+ selectionModel()->select( selectionModel()->selection(),
+ QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ QAbstractItemView::startDrag( supportedActions );
+ }
+
+void FieldsView::setModel( FieldsListModel* aModel )
+ {
+ QTableView::setModel( aModel );
+ qRegisterMetaType< QList<QPersistentModelIndex> >();
+ connect( aModel, SIGNAL(indexesDropped(QList<QPersistentModelIndex>)),
+ this, SLOT(selectIndexes(QList<QPersistentModelIndex>)), Qt::QueuedConnection );
+ }
+
+void FieldsView::selectIndexes( QList<QPersistentModelIndex> aIndexes )
+ {
+ selectionModel()->clearSelection();
+ foreach( QPersistentModelIndex pIndex, aIndexes )
+ selectionModel()->select( pIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ }
diff --git a/src/dic-options/FieldsView.h b/src/dic-options/FieldsView.h
new file mode 100644
index 0000000..6944474
--- /dev/null
+++ b/src/dic-options/FieldsView.h
@@ -0,0 +1,24 @@
+#ifndef FIELDSVIEW_H
+#define FIELDSVIEW_H
+
+#include <QTableView>
+
+#include "FieldsListModel.h"
+
+class FieldsView : public QTableView
+{
+ Q_OBJECT
+public:
+ FieldsView(QWidget *parent = 0);
+
+ FieldsListModel* model() const
+ { return qobject_cast<FieldsListModel*>(QAbstractItemView::model()); }
+
+ void startDrag(Qt::DropActions supportedActions);
+ void setModel(FieldsListModel *aModel);
+public slots:
+ void selectIndexes(QList<QPersistentModelIndex> aIndexes);
+
+};
+
+#endif // FIELDSVIEW_H
diff --git a/src/dic-options/PackFieldsListModel.cpp b/src/dic-options/PackFieldsListModel.cpp
new file mode 100644
index 0000000..eb6eb6f
--- /dev/null
+++ b/src/dic-options/PackFieldsListModel.cpp
@@ -0,0 +1,101 @@
+#include "PackFieldsListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Field.h"
+
+#include <QMimeData>
+
+int PackFieldsListModel::rowCount( const QModelIndex& /*parent*/ ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return 0;
+ return pack->getFields().size();
+ }
+
+QVariant PackFieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return QVariant();
+ const Field* field = pack->getFields().value(index.row());
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return field->name();
+ case Qt::FontRole:
+ if( index.row() == 0 )
+ {
+ QFont font;
+ font.setBold(true);
+ return font;
+ }
+ else
+ return QFont();
+ default:
+ return QVariant();
+ }
+ }
+
+void PackFieldsListModel::changeParentRow( const QModelIndex& aIndex )
+ {
+ m_parentRow = aIndex.row();
+ emit layoutChanged();
+ }
+
+bool PackFieldsListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ const Field* field = m_parent->m_dict.field( aValue.toString() );
+ if( !field )
+ return false;
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ pack->setField( index.row(), field );
+ emit dataChanged(index, index);
+ return true;
+ }
+
+bool PackFieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ for (int row = 0; row < rows; ++row)
+ pack->insertField( position, m_parent->m_emptyField );
+ endInsertRows();
+ return true;
+ }
+
+bool PackFieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ for (int row = 0; row < rows; ++row)
+ pack->removeField( position );
+ endRemoveRows();
+ return true;
+ }
+
+const void* PackFieldsListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return NULL;
+ const Field* field = pack->getFields().value( aIndex.row() );
+ return static_cast<const void*>( field );
+ }
+
+void PackFieldsListModel::insertPointer( int aPos, void* aData )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos );
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return;
+ pack->insertField( aPos, static_cast<Field*>( aData ) );
+ endInsertRows();
+ }
diff --git a/src/dic-options/PackFieldsListModel.h b/src/dic-options/PackFieldsListModel.h
new file mode 100644
index 0000000..06565c5
--- /dev/null
+++ b/src/dic-options/PackFieldsListModel.h
@@ -0,0 +1,34 @@
+#ifndef PACKFIELDSLISTMODEL_H
+#define PACKFIELDSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+#include "../dictionary/Dictionary.h"
+
+class PackFieldsListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ PackFieldsListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent ), m_parentRow( 0 )
+ {}
+
+ int rowCount( const QModelIndex& parent = QModelIndex() ) const;
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+
+public slots:
+ void changeParentRow( const QModelIndex& aIndex );
+
+private:
+ int m_parentRow;
+};
+
+#endif
diff --git a/src/dic-options/PackFieldsView.cpp b/src/dic-options/PackFieldsView.cpp
new file mode 100644
index 0000000..6925984
--- /dev/null
+++ b/src/dic-options/PackFieldsView.cpp
@@ -0,0 +1,36 @@
+#include "PackFieldsView.h"
+
+PackFieldsView::PackFieldsView(QWidget *parent):
+ QListView( parent )
+ {
+ setDragEnabled(true);
+ setAcceptDrops(true);
+ setDropIndicatorShown(true);
+ setDragDropOverwriteMode( false );
+ }
+
+void PackFieldsView::setModel( QAbstractItemModel* aModel )
+ {
+ QListView::setModel( aModel );
+ qRegisterMetaType< QList<QPersistentModelIndex> >();
+ connect( aModel, SIGNAL(indexesDropped(QList<QPersistentModelIndex>)),
+ this, SLOT(selectIndexes(QList<QPersistentModelIndex>)), Qt::QueuedConnection );
+ }
+
+void PackFieldsView::selectIndexes( QList<QPersistentModelIndex> aIndexes )
+ {
+ selectionModel()->clearSelection();
+ foreach( QPersistentModelIndex pIndex, aIndexes )
+ {
+ QModelIndex index = model()->index( pIndex.row(), 0, pIndex.parent() );
+ selectionModel()->select( index, QItemSelectionModel::Select );
+ }
+ selectionModel()->setCurrentIndex( aIndexes[0], QItemSelectionModel::NoUpdate );
+ }
+
+void PackFieldsView::updateCurrent()
+ {
+ QItemSelection selection = selectionModel()->selection();
+ reset();
+ selectionModel()->select( selection, QItemSelectionModel::Select );
+ }
diff --git a/src/dic-options/PackFieldsView.h b/src/dic-options/PackFieldsView.h
new file mode 100644
index 0000000..7c4aeea
--- /dev/null
+++ b/src/dic-options/PackFieldsView.h
@@ -0,0 +1,24 @@
+#ifndef PACKFIELDSVIEW_H
+#define PACKFIELDSVIEW_H
+
+#include <QListView>
+
+#include "PackFieldsListModel.h"
+#include "DraggableListModel.h"
+
+class PackFieldsView : public QListView
+{
+ Q_OBJECT
+public:
+ PackFieldsView(QWidget *parent = 0);
+
+ void setModel(QAbstractItemModel *aModel);
+ DraggableListModel* model() const
+ { return qobject_cast<DraggableListModel*>(QAbstractItemView::model()); }
+public slots:
+ void selectIndexes(QList<QPersistentModelIndex> aIndexes);
+ void updateCurrent();
+
+};
+
+#endif
diff --git a/src/dic-options/PacksListModel.cpp b/src/dic-options/PacksListModel.cpp
new file mode 100644
index 0000000..c2dfc31
--- /dev/null
+++ b/src/dic-options/PacksListModel.cpp
@@ -0,0 +1,81 @@
+#include "PacksListModel.h"
+#include "../dictionary/CardPack.h"
+
+QVariant PacksListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ {
+ const CardPack* pack = m_parent->m_dict.cardPack( index.row() );
+ if( !pack )
+ return QVariant();
+ return pack->id();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+bool PacksListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ m_parent->m_dict.cardPack( index.row() )->setName( aValue.toString() );
+ emit dataChanged(index, index);
+ return true;
+ }
+
+Qt::ItemFlags PacksListModel::flags(const QModelIndex &index) const
+ {
+ Qt::ItemFlags defaultFlags = DraggableListModel::flags(index);
+ return defaultFlags;
+ }
+
+bool PacksListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int i = 0; i < rows; i++)
+ {
+ CardPack* newPack = new CardPack( &(m_parent->m_dict) );
+ newPack->addField( m_parent->m_dict.field(0) );
+ m_parent->m_dict.insertPack( position + i, newPack );
+ }
+ endInsertRows();
+ return true;
+ }
+
+bool PacksListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_parent->m_dict.removePack( position );
+ endRemoveRows();
+ return true;
+ }
+
+const void* PacksListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ CardPack* pack = m_parent->m_dict.cardPack( aIndex.row() );
+ return static_cast<const void*>( pack );
+ }
+
+void PacksListModel::insertPointer( int aPos, void* aData )
+ {
+ beginInsertRows(QModelIndex(), aPos, aPos );
+ m_parent->m_dict.insertPack( aPos, static_cast<CardPack*>( aData ) );
+ endInsertRows();
+ }
+
+void PacksListModel::removePack( int aPos )
+ {
+ beginRemoveRows(QModelIndex(), aPos, aPos);
+ m_parent->m_dict.destroyPack( aPos );
+ endRemoveRows();
+ }
diff --git a/src/dic-options/PacksListModel.h b/src/dic-options/PacksListModel.h
new file mode 100644
index 0000000..3523912
--- /dev/null
+++ b/src/dic-options/PacksListModel.h
@@ -0,0 +1,32 @@
+#ifndef PACKSLISTMODEL_H
+#define PACKSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+#include "../dictionary/Dictionary.h"
+
+class PacksListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ PacksListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->m_dict.cardPacksNum(); }
+
+ QVariant data( const QModelIndex &index, int role ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+ void removePack( int aPos );
+};
+
+#endif
diff --git a/src/dic-options/PacksPage.cpp b/src/dic-options/PacksPage.cpp
new file mode 100644
index 0000000..e78bbf0
--- /dev/null
+++ b/src/dic-options/PacksPage.cpp
@@ -0,0 +1,345 @@
+#include "PacksPage.h"
+#include "FieldsListModel.h"
+#include "PacksListModel.h"
+#include "PackFieldsListModel.h"
+#include "PackFieldsView.h"
+#include "UnusedFieldsListModel.h"
+#include "../dictionary/Dictionary.h"
+#include "../study/CardSideView.h"
+#include "../dictionary/CardPack.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QLabel>
+#include <QPushButton>
+#include <QToolButton>
+#include <QStringListModel>
+
+PacksPage::PacksPage( DictionaryOptionsDialog* aParent ):
+ QWidget( aParent ), m_parent( aParent )
+ {
+ createPacksList();
+ createPackFieldsList();
+ createUnusedFieldsList();
+ createPackPreview();
+
+ QGridLayout* mainLt = new QGridLayout;
+ mainLt->addLayout( m_packsListLt, 0, 0, 2, 1 );
+ mainLt->addLayout( m_fieldsListLt, 0, 1 );
+ mainLt->addLayout( m_unusedFieldsListLt, 1, 1 );
+ mainLt->addLayout( m_previewLt, 0, 2, 2, 1 );
+ setLayout( mainLt );
+
+ // Select first pack
+ m_packsListView->selectionModel()->setCurrentIndex( m_packFieldsListModel->index(0, 0),
+ QItemSelectionModel::Select );
+ }
+
+PacksPage::~PacksPage()
+ {
+ }
+
+void PacksPage::createPacksList()
+ {
+ m_packsListModel = new PacksListModel( m_parent );
+ m_packsListView = new PackFieldsView( this );
+ m_packsListView->setModel( m_packsListModel );
+ m_packsListView->setDropIndicatorShown(true);
+ m_packsListView->setMinimumWidth( 200 );
+
+ QPushButton* addPackBtn = new QPushButton( QPixmap(":/images/add.png"), tr("Add") );
+ QPushButton* removePackBtn = new QPushButton( QPixmap(":/images/delete.png"), tr("Remove") );
+ QHBoxLayout* packBtnLt = new QHBoxLayout;
+ packBtnLt->addWidget( addPackBtn );
+ packBtnLt->addWidget( removePackBtn );
+
+ connect( addPackBtn, SIGNAL(clicked()), this, SLOT(addPack()) );
+ connect( removePackBtn, SIGNAL(clicked()), this, SLOT(removePacks()) );
+
+ QToolButton* packMoveUpBtn = new QToolButton;
+ packMoveUpBtn->setObjectName("pack-up");
+ packMoveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ packMoveUpBtn->setToolTip(tr("Move pack up"));
+ QToolButton* packMoveDownBtn = new QToolButton;
+ packMoveDownBtn->setObjectName("pack-down");
+ packMoveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ packMoveDownBtn->setToolTip(tr("Move pack down"));
+ QVBoxLayout* packMoveBtnsLt = new QVBoxLayout;
+ packMoveBtnsLt->addWidget( packMoveUpBtn );
+ packMoveBtnsLt->addWidget( packMoveDownBtn );
+ packMoveBtnsLt->addStretch( 1 );
+
+ connect( packMoveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( packMoveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_packsListLt = new QGridLayout;
+ m_packsListLt->addWidget( new QLabel("<b>"+tr("Card packs")+"</b>"), 0, 0, 1, 2 );
+ m_packsListLt->addWidget( m_packsListView, 1, 0 );
+ m_packsListLt->addLayout( packMoveBtnsLt, 1, 1 );
+ m_packsListLt->addLayout( packBtnLt, 2, 0, 1, 2 );
+ }
+
+void PacksPage::createPackFieldsList()
+ {
+ m_packFieldsListModel = new PackFieldsListModel( m_parent );
+ m_fieldsListView = new PackFieldsView( this );
+ m_fieldsListView->setModel( m_packFieldsListModel );
+ m_fieldsListView->setDropIndicatorShown(true);
+
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ m_packFieldsListModel, SLOT(changeParentRow(const QModelIndex&)) );
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateUsesExactAnswer(const QModelIndex&)) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_packsListView, SLOT(updateCurrent()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_packsListView, SLOT(updateCurrent()) );
+
+ QToolButton* fieldMoveUpBtn = new QToolButton;
+ fieldMoveUpBtn->setObjectName("field-up");
+ fieldMoveUpBtn->setIcon(QIcon(":/images/1uparrow.png"));
+ fieldMoveUpBtn->setToolTip(tr("Move field up"));
+ QToolButton* fieldMoveDownBtn = new QToolButton;
+ fieldMoveDownBtn->setObjectName("field-down");
+ fieldMoveDownBtn->setIcon(QIcon(":/images/1downarrow.png"));
+ fieldMoveDownBtn->setToolTip(tr("Move field down"));
+ QVBoxLayout* fieldBtnsLt = new QVBoxLayout;
+ fieldBtnsLt->addWidget( fieldMoveUpBtn );
+ fieldBtnsLt->addWidget( fieldMoveDownBtn );
+ fieldBtnsLt->addStretch( 1 );
+
+ connect( fieldMoveUpBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+ connect( fieldMoveDownBtn, SIGNAL(clicked()), this, SLOT(moveItemsUpDown()) );
+
+ m_fieldsListLt = new QGridLayout;
+ m_fieldsListLt->addWidget( new QLabel("<b>"+tr("Pack fields")+"</b>"), 0, 0, 1, 2 );
+ m_fieldsListLt->addWidget( m_fieldsListView, 1, 0 );
+ m_fieldsListLt->addLayout( fieldBtnsLt, 1, 1 );
+
+ }
+
+void PacksPage::createUnusedFieldsList()
+ {
+ m_unusedFieldsListModel = new UnusedFieldsListModel( m_parent );
+ m_unusedFieldsListView = new PackFieldsView( this );
+ m_unusedFieldsListView->setModel( m_unusedFieldsListModel );
+
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ m_unusedFieldsListModel, SLOT(changeParentRow(const QModelIndex&)) );
+
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ m_unusedFieldsListModel, SLOT(updateUnusedFields()) );
+
+ QToolButton* fieldRemoveBtn = new QToolButton;
+ fieldRemoveBtn->setIcon(QIcon(":/images/down.png"));
+ fieldRemoveBtn->setToolTip(tr("Remove field from pack"));
+ QToolButton* fieldAddBtn = new QToolButton;
+ fieldAddBtn->setIcon(QIcon(":/images/up.png"));
+ fieldAddBtn->setToolTip(tr("Add field to pack"));
+ QHBoxLayout* usedFieldsBtnsLt = new QHBoxLayout;
+ usedFieldsBtnsLt->addWidget( fieldRemoveBtn );
+ usedFieldsBtnsLt->addWidget( fieldAddBtn );
+
+ connect( fieldRemoveBtn, SIGNAL(clicked()), this, SLOT(removeFields()) );
+ connect( fieldAddBtn, SIGNAL(clicked()), this, SLOT(addFields()) );
+
+ usesExactAnswerBox = new QCheckBox(tr("Uses exact answer"));
+ connect(usesExactAnswerBox,
+ SIGNAL(stateChanged(int)), SLOT(updatePackUsesExactAnswer(int)));
+
+ m_unusedFieldsListLt = new QGridLayout;
+ m_unusedFieldsListLt->addLayout( usedFieldsBtnsLt, 0, 0 );
+ m_unusedFieldsListLt->addWidget( new QLabel("<b>"+tr("Unused fields")+"</b>"), 0, 1 );
+ m_unusedFieldsListLt->addWidget( m_unusedFieldsListView, 1, 0, 1, 2 );
+ m_unusedFieldsListLt->addWidget(usesExactAnswerBox, 2, 0, 1, 2);
+ }
+
+void PacksPage::createPackPreview()
+ {
+ m_qstPreview = new CardSideView( CardSideView::QstMode );
+ m_qstPreview->setMinimumSize( 200, 70 );
+ m_ansPreview = new CardSideView( CardSideView::AnsMode );
+ m_ansPreview->setMinimumSize( 200, 70 );
+ m_previewLt = new QVBoxLayout;
+ m_previewLt->addWidget( new QLabel( "<b>"+tr("Preview")+"</b>") );
+ m_previewLt->addWidget( m_qstPreview, 1 );
+ m_previewLt->addWidget( m_ansPreview, 1 );
+
+ // From: the packs list
+ connect( m_packsListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updatePreviewForPack()) );
+
+ // From: the pack fields list
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+ connect( m_packFieldsListModel,
+ SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+
+ // From: the dictionary field list at the "Fields" page
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updatePreviewForPack()) );
+ connect( m_parent->m_fieldsListModel,
+ SIGNAL(rowsRemoved(const QModelIndex&, int, int)),
+ SLOT(updatePreviewForPack()) );
+ }
+
+void PacksPage::updatePreviewForPack()
+ {
+ int selectedPack = m_packsListView->selectionModel()->currentIndex().row();
+ if( selectedPack > -1 ) // If any selection
+ m_curPack = selectedPack;
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ if( !pack )
+ return;
+
+ const Field* qstField = pack->getQuestionField();
+ if( !qstField )
+ return;
+ QStringList ansList;
+ foreach( const Field* field, pack->getAnswerFields() )
+ ansList << field->name();
+ m_qstPreview->setPack( pack );
+ m_qstPreview->setQuestion( qstField->name() );
+ m_ansPreview->setPack( pack );
+ m_ansPreview->setQstAnsr( qstField->name(), ansList );
+ }
+
+void PacksPage::moveItemsUpDown()
+ {
+ PackFieldsView* view;
+ if( sender()->objectName().contains("pack") )
+ view = m_packsListView;
+ else if( sender()->objectName().contains("field") )
+ view = m_fieldsListView;
+ else
+ return;
+ DraggableListModel* model = view->model();
+
+ QModelIndexList selectedIndexes = view->selectionModel()->selectedIndexes();
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pSrcIndexes << QPersistentModelIndex( idx );
+ int rowsNum = pSrcIndexes.size();
+ if( rowsNum == 0 )
+ return;
+ qSort( pSrcIndexes );
+
+ int beginRow;
+ if( sender()->objectName().contains("up") )
+ beginRow = pSrcIndexes[0].row()-1;
+ else if( sender()->objectName().contains("down") )
+ beginRow = pSrcIndexes[rowsNum-1].row()+2;
+ else
+ return;
+ if( rowsNum == 1 && (beginRow < 0 || beginRow > model->rowCount()) )
+ return;
+ if( beginRow < 0 )
+ beginRow = 0;
+ if( beginRow > model->rowCount() )
+ beginRow = model->rowCount();
+
+ QMimeData* mime = model->mimeData( selectedIndexes );
+ model->dropMimeData( mime, Qt::MoveAction, beginRow, 0, QModelIndex() );
+ foreach (QModelIndex index, pSrcIndexes)
+ model->removeRow( index.row() );
+ }
+
+void PacksPage::removeFields()
+ {
+ if( m_packFieldsListModel->rowCount() <= 1 )
+ return;
+ QModelIndexList selectedIndexes = m_fieldsListView->selectionModel()->selectedIndexes();
+ if( selectedIndexes.isEmpty() )
+ return;
+ QList<QPersistentModelIndex> pSrcIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pSrcIndexes << QPersistentModelIndex( idx );
+ foreach (QModelIndex index, pSrcIndexes)
+ m_fieldsListView->model()->removeRow( index.row() );
+ }
+
+void PacksPage::addFields()
+ {
+ QModelIndexList selectedIndexes = m_unusedFieldsListView->selectionModel()->selectedIndexes();
+ if( selectedIndexes.isEmpty() )
+ return;
+ int dstRow = m_fieldsListView->currentIndex().row()+1;
+ QMimeData* mime = m_unusedFieldsListModel->mimeData( selectedIndexes );
+ m_packFieldsListModel->dropMimeData( mime, Qt::MoveAction, dstRow, 0, QModelIndex() );
+ }
+
+
+void PacksPage::addPack()
+ {
+ QModelIndexList selectedIndexes = m_packsListView->selectionModel()->selectedRows();
+ int row;
+ if( selectedIndexes.size() > 0 )
+ {
+ qSort(selectedIndexes);
+ row = selectedIndexes[selectedIndexes.size()-1].row() + 1;
+ }
+ else
+ row = 0;
+ m_packsListModel->insertRow( row, QModelIndex() );
+ }
+
+void PacksPage::removePacks()
+ {
+ QModelIndexList selectedIndexes = m_packsListView->selectionModel()->selectedRows();
+ QList<QPersistentModelIndex> pIndexes;
+ foreach (QModelIndex idx, selectedIndexes)
+ if( idx.isValid() )
+ pIndexes << QPersistentModelIndex( idx );
+ if( pIndexes.size() == 0 )
+ return;
+ foreach( QModelIndex idx, pIndexes )
+ m_packsListModel->removePack( idx.row() );
+ }
+
+void PacksPage::renamePack()
+ {
+ QModelIndex idx = m_packsListView->currentIndex();
+ if( !idx.isValid() )
+ return;
+ m_packsListView->edit( idx );
+ }
+
+void PacksPage::updateUsesExactAnswer(const QModelIndex& index)
+{
+ int selectedPack = index.row();
+ if( selectedPack > -1 ) // If any selection
+ m_curPack = selectedPack;
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ if( !pack )
+ return;
+ usesExactAnswerBox->setChecked(pack->getUsesExactAnswer());
+}
+
+void PacksPage::updatePackUsesExactAnswer(int state)
+{
+ CardPack* pack = m_parent->m_dict.cardPack( m_curPack );
+ pack->setUsesExactAnswer(state == Qt::Checked);
+}
diff --git a/src/dic-options/PacksPage.h b/src/dic-options/PacksPage.h
new file mode 100644
index 0000000..e191367
--- /dev/null
+++ b/src/dic-options/PacksPage.h
@@ -0,0 +1,70 @@
+#ifndef PACKSPAGE_H
+#define PACKSPAGE_H
+
+#include <QWidget>
+#include <QAbstractListModel>
+#include <QModelIndex>
+#include <QListView>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QGridLayout>
+
+#include "DictionaryOptionsDialog.h"
+#include "PackFieldsView.h"
+
+class CardSideView;
+class Dictionary;
+class PacksListModel;
+class UnusedFieldsListModel;
+
+class PacksPage : public QWidget
+{
+ Q_OBJECT
+public:
+ PacksPage( DictionaryOptionsDialog* aParent );
+ ~PacksPage();
+
+public slots:
+ void moveItemsUpDown();
+ void removeFields();
+ void addFields();
+ void addPack();
+ void removePacks();
+ void renamePack();
+
+private:
+ void createPacksList();
+ void createPackFieldsList();
+ void createUnusedFieldsList();
+ void createPackPreview();
+
+private slots:
+ void updatePreviewForPack();
+ void updateUsesExactAnswer(const QModelIndex& index);
+ void updatePackUsesExactAnswer(int state);
+
+private:
+ DictionaryOptionsDialog* m_parent;
+
+ // Models
+ PacksListModel* m_packsListModel;
+ QAbstractListModel* m_packFieldsListModel;
+ UnusedFieldsListModel* m_unusedFieldsListModel;
+ int m_curPack;
+
+ // List views
+ PackFieldsView* m_packsListView;
+ PackFieldsView* m_fieldsListView;
+ PackFieldsView* m_unusedFieldsListView;
+ QCheckBox* usesExactAnswerBox;
+ CardSideView* m_qstPreview;
+ CardSideView* m_ansPreview;
+
+ // Layouts
+ QGridLayout* m_packsListLt;
+ QGridLayout* m_fieldsListLt;
+ QGridLayout* m_unusedFieldsListLt;
+ QVBoxLayout* m_previewLt;
+};
+
+#endif
diff --git a/src/dic-options/UnusedFieldsListModel.cpp b/src/dic-options/UnusedFieldsListModel.cpp
new file mode 100644
index 0000000..3311285
--- /dev/null
+++ b/src/dic-options/UnusedFieldsListModel.cpp
@@ -0,0 +1,111 @@
+#include "UnusedFieldsListModel.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Dictionary.h"
+
+#include <QMimeData>
+
+UnusedFieldsListModel::UnusedFieldsListModel( DictionaryOptionsDialog* aParent ):
+ DraggableListModel( aParent ), m_parentRow( 0 )
+ {
+ updateUnusedFields();
+ }
+
+
+int UnusedFieldsListModel::rowCount(const QModelIndex& /*parent*/) const
+ {
+ return m_unusedFields.size();
+ }
+
+QVariant UnusedFieldsListModel::data( const QModelIndex &index, int role ) const
+ {
+ if (!index.isValid())
+ return QVariant();
+ if (index.row() >= rowCount())
+ return QVariant();
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return m_unusedFields.value(index.row())->name();
+ default:
+ return QVariant();
+ }
+ }
+
+void UnusedFieldsListModel::updateUnusedFields()
+ {
+ beginResetModel();
+ m_unusedFields.clear();
+ CardPack* pack = m_parent->m_dict.cardPack( m_parentRow );
+ if( !pack )
+ return;
+ QList<const Field*> packFields = pack->getFields();
+ for( int i=0; i < m_parent->m_dict.fieldsNum(); i++ )
+ {
+ const Field* dictField = m_parent->m_dict.field( i );
+ if( !packFields.contains( dictField ) )
+ m_unusedFields << dictField;
+ }
+ endResetModel();
+ }
+
+void UnusedFieldsListModel::changeParentRow( const QModelIndex& aIndex )
+ {
+ m_parentRow = aIndex.row();
+ updateUnusedFields();
+ emit layoutChanged();
+ }
+
+bool UnusedFieldsListModel::setData(const QModelIndex& index, const QVariant& aValue, int role)
+ {
+ if( !index.isValid() || role != Qt::EditRole )
+ return false;
+ const Field* field = m_parent->m_dict.field( aValue.toString() );
+ if( !field )
+ return false;
+ m_unusedFields[index.row()] = field;
+ emit dataChanged(index, index);
+ return true;
+ }
+
+bool UnusedFieldsListModel::insertRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginInsertRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_unusedFields.insert( position, m_parent->m_emptyField );
+ endInsertRows();
+ updateUnusedFields();
+ return true;
+ }
+
+bool UnusedFieldsListModel::removeRows(int position, int rows, const QModelIndex &/*parent*/)
+ {
+ beginRemoveRows(QModelIndex(), position, position+rows-1);
+ for (int row = 0; row < rows; ++row)
+ m_unusedFields.removeAt( position );
+ endRemoveRows();
+ updateUnusedFields();
+ return true;
+ }
+
+const void* UnusedFieldsListModel::dataPtr( const QModelIndex& aIndex ) const
+ {
+ const Field* field = m_unusedFields.value( aIndex.row() );
+ return static_cast<const void*>( field );
+ }
+
+void UnusedFieldsListModel::insertPointer( int aPos, void* aData )
+ {
+ m_unusedFields.insert( aPos, static_cast<Field*>( aData ) );
+ }
+
+bool UnusedFieldsListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
+ int /*row*/, int /*column*/, const QModelIndex &/*parent*/)
+ {
+ if (action == Qt::IgnoreAction)
+ return true;
+ if (!data->hasFormat("application/octet-stream"))
+ return false;
+ return true;
+ }
diff --git a/src/dic-options/UnusedFieldsListModel.h b/src/dic-options/UnusedFieldsListModel.h
new file mode 100644
index 0000000..c3a95f6
--- /dev/null
+++ b/src/dic-options/UnusedFieldsListModel.h
@@ -0,0 +1,35 @@
+#ifndef UNUSEDFIELDSLISTMODEL_H
+#define UNUSEDFIELDSLISTMODEL_H
+
+#include <QAbstractListModel>
+
+#include "DictionaryOptionsDialog.h"
+#include "DraggableListModel.h"
+
+class UnusedFieldsListModel : public DraggableListModel
+{
+ Q_OBJECT
+
+public:
+ UnusedFieldsListModel( DictionaryOptionsDialog* aParent );
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const;
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ bool setData(const QModelIndex& index, const QVariant& aValue, int role = Qt::EditRole );
+ bool insertRows(int position, int rows, const QModelIndex &);
+ bool removeRows(int position, int rows, const QModelIndex &);
+ const void* dataPtr( const QModelIndex& aIndex ) const;
+ void insertPointer( int aPos, void* aData );
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
+ const QModelIndex &parent);
+
+public slots:
+ void changeParentRow( const QModelIndex& aIndex );
+ void updateUnusedFields();
+
+private:
+ int m_parentRow;
+ QList<const Field*> m_unusedFields; ///< Always updated list
+};
+
+#endif
diff --git a/src/dictionary/Card.cpp b/src/dictionary/Card.cpp
new file mode 100644
index 0000000..6464941
--- /dev/null
+++ b/src/dictionary/Card.cpp
@@ -0,0 +1,125 @@
+#include <QtDebug>
+
+#include "Card.h"
+#include "ICardPack.h"
+#include "DicRecord.h"
+#include "Field.h"
+
+bool Card::operator==( const Card& another ) const
+ {
+ return question == another.getQuestion();
+ }
+
+/** Card name is its question.
+ Image tags are replaced with image names.
+ */
+QString Card::getName() const
+ {
+ QString nameStr = question;
+ QRegExp imageNameRx("<img\\s*src=\"(.+[/\\\\])?(.+)\".*>");
+ nameStr.replace( imageNameRx, "\\2");
+ return nameStr;
+ }
+
+QStringList Card::getAnswers()
+ {
+ generateAnswers();
+ return answers;
+ }
+
+QList<const DicRecord*> Card::getSourceRecords()
+ {
+ if( sourceRecords.isEmpty() )
+ generateAnswers();
+ return sourceRecords;
+ }
+
+QMultiHash<QString, QString> Card::getAnswerElements()
+{
+ QMultiHash<QString, QString> answerElements; // Key: field name
+ QString qstFieldName = cardPack->getQuestionFieldName();
+ foreach(DicRecord* record, cardPack->getRecords())
+ if(record->getFieldElements(qstFieldName).contains(question) &&
+ record->isValid(qstFieldName))
+ {
+ sourceRecords << record;
+ connect(record, SIGNAL(valueChanged(QString)), SLOT(dropAnswers()));
+ foreach(QString ansFieldName, cardPack->getAnswerFieldNames())
+ {
+ if(record->getFieldValue(ansFieldName).isEmpty())
+ continue;
+ foreach(QString ansElement, record->getFieldElements(ansFieldName))
+ if(!answerElements.contains(ansFieldName, ansElement))
+ answerElements.insert(ansFieldName, ansElement);
+ }
+ }
+ return answerElements;
+}
+
+void Card::generateAnswersFromElements(
+ const QMultiHash<QString, QString>& answerElements)
+{
+ foreach(const Field* ansField, cardPack->getAnswerFields())
+ {
+ if(!ansField)
+ {
+ answers << "";
+ continue;
+ }
+ QStringList elements = getAnswerElementsForField(answerElements, ansField->name());
+ if(elements.isEmpty())
+ {
+ answers << "";
+ continue;
+ }
+ answers << elements.join(ICardPack::HomonymDelimiter);
+ }
+}
+
+QStringList Card::getAnswerElementsForField(const QMultiHash<QString, QString>& answerElements,
+ const QString& fieldName) const
+{
+ QStringList elements = answerElements.values(fieldName); // reversed order
+ QStringList elementsInOrder;
+ elementsInOrder.reserve(elements.size());
+ std::reverse_copy( elements.begin(), elements.end(),
+ std::back_inserter(elementsInOrder) );
+ return elementsInOrder;
+}
+
+// Fills also list of source entries
+void Card::generateAnswers()
+ {
+ if(!cardPack)
+ return;
+ clearAnswers();
+ generateAnswersFromElements(getAnswerElements());
+ }
+
+void Card::clearAnswers()
+{
+ answers.clear();
+ foreach(const DicRecord* record, sourceRecords)
+ disconnect( record, 0, this, SLOT(dropAnswers()) );
+ sourceRecords.clear();
+}
+
+void Card::dropAnswers()
+ {
+ clearAnswers();
+ emit answersChanged();
+ }
+
+StudyRecord Card::getStudyRecord() const
+ {
+ return cardPack->getStudyRecord(getQuestion());
+ }
+
+///< Reviewed at least twice
+bool Card::isScheduledAndReviewed() const
+ {
+ if(!cardPack)
+ return false;
+ int reviews = cardPack->getStudyRecords(getQuestion()).size();
+ return reviews >= 2;
+ }
diff --git a/src/dictionary/Card.h b/src/dictionary/Card.h
new file mode 100644
index 0000000..bd822e7
--- /dev/null
+++ b/src/dictionary/Card.h
@@ -0,0 +1,50 @@
+#ifndef CARD_H
+#define CARD_H
+
+#include <QtCore>
+
+#include "../study/StudyRecord.h"
+
+class Dictionary;
+class DicRecord;
+class ICardPack;
+
+class Card: public QObject
+{
+Q_OBJECT
+
+public:
+ Card(const ICardPack* cardPack, QString qst = ""):
+ cardPack(cardPack), question(qst) {}
+
+ bool operator==( const Card& another ) const;
+ QString getName() const;
+ const ICardPack* getCardPack() const { return cardPack; }
+ QString getQuestion() const { return question; }
+ QStringList getAnswers();
+ QList<const DicRecord*> getSourceRecords();
+ StudyRecord getStudyRecord() const;
+ bool isScheduledAndReviewed() const;
+
+private:
+ void generateAnswers();
+ void clearAnswers();
+ QMultiHash<QString, QString> getAnswerElements();
+ void generateAnswersFromElements(const QMultiHash<QString, QString>& answerElements);
+ QStringList getAnswerElementsForField(const QMultiHash<QString, QString>& answerElements,
+ const QString& fieldName) const;
+
+private slots:
+ void dropAnswers();
+
+signals:
+ void answersChanged() const;
+
+private:
+ const ICardPack* cardPack;
+ QString question;
+ QStringList answers;
+ QList<const DicRecord*> sourceRecords;
+};
+
+#endif
diff --git a/src/dictionary/CardPack.cpp b/src/dictionary/CardPack.cpp
new file mode 100644
index 0000000..75f5924
--- /dev/null
+++ b/src/dictionary/CardPack.cpp
@@ -0,0 +1,432 @@
+#include "CardPack.h"
+#include "Field.h"
+#include "DicRecord.h"
+#include "Card.h"
+#include "../study/StudyRecord.h"
+#include "../study/StudySettings.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+const QString CardPack::SynonymDelimiter = ";";
+const QString CardPack::HomonymDelimiter = "; ";
+
+CardPack::CardPack(IDictionary* dict):
+ m_dictionary(dict), isReadingStudyFile(false), usesExactAnswer(false)
+{
+ enableDictRecordUpdates();
+}
+
+/// Copies fields and study data from another pack.
+CardPack::CardPack(IDictionary* dict, const CardPack* otherPack ):
+ m_dictionary(dict)
+{
+ enableDictRecordUpdates();
+
+ // Copy fields by name
+ foreach( const Field* otherField, otherPack->fields )
+ {
+ Q_ASSERT( otherField );
+ if( !otherField )
+ return;
+ const Field* field = m_dictionary->field( otherField->name() );
+ fields << field;
+ }
+
+ // Copy study data
+ m_curCardName = otherPack->m_curCardName;
+ studyRecords = otherPack->studyRecords;
+ usesExactAnswer = otherPack->usesExactAnswer;
+}
+
+CardPack::~CardPack()
+ {
+ foreach( Card* card, cards )
+ delete card;
+ }
+
+QList<DicRecord*> CardPack::getRecords() const
+ {
+ if(!m_dictionary)
+ return QList<DicRecord*>();
+ return m_dictionary->getRecords();
+ }
+
+const TreeItem* CardPack::parent() const
+ {
+ return dynamic_cast<const TreeItem*>(m_dictionary);
+ }
+
+QVariant CardPack::data( int aColumn ) const
+ {
+ switch( aColumn )
+ {
+ case 0:
+ return id();
+ case 1:
+ return getActiveCards().size();
+ case 2:
+ return getNewCards().size();
+ default:
+ return QVariant();
+ }
+ }
+
+int CardPack::row() const
+ {
+ return m_dictionary->indexOfCardPack( const_cast<CardPack*>(this) );
+ }
+
+int CardPack::topParentRow() const
+ {
+ return dynamic_cast<TreeItem*>(m_dictionary)->row();
+ }
+
+/** Contains cards with at least 1 study record
+ */
+bool CardPack::containsReviewedCards() const
+ {
+ foreach( QString cardId, studyRecords.uniqueKeys() )
+ if( studyRecords.values( cardId ).size() >= 1 )
+ return true;
+ return false;
+ }
+
+StudyRecord CardPack::getStudyRecord(QString cardId) const
+{
+ QList<StudyRecord> recordList = studyRecords.values(cardId);
+ return recordList.isEmpty() ? StudyRecord() : recordList.first();
+}
+
+QString CardPack::findLastReviewedCard() const
+ {
+ QDateTime lastReview;
+ QString lastCardName;
+ foreach( QString cardName, cardQuestions )
+ {
+ QDateTime reviewed = getStudyRecord( cardName ).date;
+ if( reviewed > lastReview )
+ {
+ lastReview = reviewed;
+ lastCardName = cardName;
+ }
+ }
+ return lastCardName;
+ }
+
+QString CardPack::id() const
+{
+ if( !m_name.isEmpty() )
+ return m_name;
+ if( fields.empty() || !getQuestionField() )
+ return tr("(empty pack)");
+ return getQuestionFieldName() + " - " + getAnswerFieldNames().join(", ");
+}
+
+const Field* CardPack::getQuestionField() const
+{
+ if(fields.empty())
+ return NULL;
+ return fields.first();
+}
+
+QList<const Field*> CardPack::getAnswerFields() const
+{
+ return fields.mid(1);
+}
+
+// If the card is not yet created, it creates the card.
+Card* CardPack::getCard(const QString& cardName)
+{
+ if(cardName.isEmpty())
+ return NULL;
+ if(!cardQuestions.contains(cardName))
+ return NULL;
+ if( cards.contains( cardName ) )
+ return cards.value( cardName );
+ else
+ {
+ Card* card = new Card( this, cardName );
+ cards.insert( cardName, card );
+ return card;
+ }
+}
+
+void CardPack::setField(int aPos, const Field *aField)
+{
+ if( aPos >= fields.size() )
+ return;
+ fields[aPos] = aField;
+}
+
+void CardPack::setQstField(const Field *aField)
+{
+ if( !fields.empty() )
+ fields[0] = aField;
+ else
+ fields << aField;
+}
+
+void CardPack::setAnsFields(QList<const Field*> aFields)
+ {
+ const Field* questionField = NULL;
+ if(!fields.isEmpty())
+ questionField = fields.first();
+ fields.clear();
+ fields << questionField << aFields;
+ }
+
+void CardPack::destroyCards()
+{
+ cardQuestions.clear();
+ foreach(Card* card, cards)
+ delete card;
+ cards.clear();
+}
+
+void CardPack::addQuestionElementsForRecord(const DicRecord* record)
+{
+ foreach(QString qstElement, record->getFieldElements(getQuestionFieldName()))
+ {
+ if(!cardQuestions.contains(qstElement))
+ cardQuestions << qstElement;
+ }
+}
+
+void CardPack::removeAbsentCards(QStringList& cardQuestions)
+{
+ QMutableListIterator<QString> cardIt(cardQuestions);
+ while(cardIt.hasNext())
+ if(!cardQuestions.contains(cardIt.next()))
+ cardIt.remove();
+}
+
+void CardPack::generateQuestions()
+ {
+ destroyCards();
+
+ if(fields.size() < MinRequiredFieldsNum)
+ return;
+ if(!getQuestionField())
+ return;
+
+ foreach(DicRecord* record, getRecords())
+ if(record->isValid(getQuestionFieldName()))
+ addQuestionElementsForRecord(record);
+
+ emit cardsGenerated();
+ }
+
+QStringList CardPack::getNewCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ if(!studyRecords.contains(cardName))
+ list << cardName;
+ return list;
+}
+
+QStringList CardPack::getActiveCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ if(getStudyRecord(cardName).timeTriggered())
+ list << cardName;
+ return list;
+}
+
+int CardPack::getActiveRepeatingCardsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.timeTriggered() && study.level == StudyRecord::Repeating)
+ count++;
+ }
+ return count;
+}
+
+int CardPack::countScheduledForTodayCards() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isActivatedToday())
+ {
+ if(study.isLearning())
+ count += study.getScheduledTodayReviews();
+ else
+ count++;
+ }
+ }
+ return count;
+}
+
+// With small intervals < 1 day
+QStringList CardPack::getPriorityActiveCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.timeTriggered() && study.interval < 1)
+ list << cardName;
+ }
+ return list;
+}
+
+QStringList CardPack::getLearningCards() const
+{
+ QStringList list;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning())
+ list << cardName;
+ }
+ return list;
+}
+
+int CardPack::getLearningReviewsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning())
+ count += study.getScheduledTodayReviews();
+ }
+ return count;
+}
+
+int CardPack::getTimeToNextLearning() const
+{
+ int minTime = 10000;
+ foreach(QString cardName, getLearningCards())
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ int left = study.getSecsToNextRepetition();
+ if(left < minTime)
+ minTime = left;
+ }
+ if(minTime == 10000)
+ minTime = 0;
+ return minTime;
+}
+
+int CardPack::getInactiveLearningReviewsNum() const
+{
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ StudyRecord study = getStudyRecord(cardName);
+ if(study.isLearning() && !study.timeTriggered())
+ count += study.getScheduledTodayReviews();
+ }
+ return count;
+}
+
+QStringList CardPack::getSmallestIntervalCards(const QStringList& priorityCards)
+{
+ QStringList smallestIntervals;
+ double smallest = 1;
+ foreach(QString name, priorityCards)
+ {
+ StudyRecord study = getStudyRecord(name);
+ if(study.interval < smallest)
+ {
+ smallest = study.interval;
+ smallestIntervals.clear();
+ smallestIntervals << name;
+ }
+ else if(study.interval == smallest)
+ smallestIntervals << name;
+ }
+ return smallestIntervals;
+}
+
+int CardPack::getTodayReviewedCardsNum() const
+{
+ const int MaxStudyDepth = 3;
+ int count = 0;
+ foreach(QString cardName, cardQuestions)
+ {
+ const QList<StudyRecord> studyList = studyRecords.values(cardName);
+ for(int j = 0; j < studyList.size(); j++)
+ {
+ if(j >= MaxStudyDepth)
+ break;
+ if(studyList[j].isReviewedToday())
+ count++;
+ }
+ }
+ return count;
+}
+
+int CardPack::getTodayNewCardsNum() const
+{
+ int count = 0;
+ foreach( QString cardName, cardQuestions )
+ {
+ const QList<StudyRecord> studyList = studyRecords.values(cardName);
+ if(studyList.isEmpty())
+ continue;
+ StudyRecord firstStudy = studyList.last();
+ if(firstStudy.isReviewedToday())
+ count++;
+ }
+ return count;
+}
+
+void CardPack::addStudyRecord( const QString aCardId, const StudyRecord& aStudyRecord )
+ {
+ if(!cardQuestions.contains(aCardId))
+ return;
+ studyRecords.insert(aCardId, aStudyRecord);
+ if(!isReadingStudyFile)
+ emit studyRecordAdded();
+ }
+
+void CardPack::setCurCard( const QString aCardId )
+ {
+ if( aCardId == m_curCardName )
+ return;
+ m_curCardName = aCardId;
+ emit studyRecordAdded(); // study is modified by the cur card
+ }
+
+QList<QDateTime> CardPack::getScheduledDates() const
+{
+ const int secsInDay = 24 * 60 * 60;
+ QList<QDateTime> scheduled;
+ foreach(QString cardName, studyRecords.uniqueKeys())
+ {
+ StudyRecord record = getStudyRecord(cardName);
+ scheduled << record.date.addSecs((int)(record.interval * secsInDay));
+ }
+ return scheduled;
+}
+
+void CardPack::processEntryChangedEvent( int aEntryIx, int aFieldIx )
+ {
+ Q_UNUSED( aEntryIx )
+ if(aFieldIx != IDictionary::AllFields)
+ generateQuestions();
+ }
+
+void CardPack::disableDictRecordUpdates()
+ {
+ disconnect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entryChanged(int,int)), this, SLOT(processEntryChangedEvent(int,int)) );
+ disconnect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entriesRemoved(int,int)), this, SLOT(processEntryChangedEvent(int)) );
+ }
+
+void CardPack::enableDictRecordUpdates()
+ {
+ connect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entryChanged(int,int)), SLOT(processEntryChangedEvent(int,int)) );
+ connect(dynamic_cast<const TreeItem*>(m_dictionary),
+ SIGNAL(entriesRemoved(int,int)), SLOT(processEntryChangedEvent(int)) );
+ // Inserting empty records doesn't regenerate cards
+ }
diff --git a/src/dictionary/CardPack.h b/src/dictionary/CardPack.h
new file mode 100644
index 0000000..fc7d335
--- /dev/null
+++ b/src/dictionary/CardPack.h
@@ -0,0 +1,144 @@
+#ifndef CARDPACK_H
+#define CARDPACK_H
+
+#include <QtCore>
+#include <QtDebug>
+
+#include "ICardPack.h"
+#include "../study/StudyRecord.h"
+#include "IDictionary.h"
+#include "Field.h"
+#include "TreeItem.h"
+
+class DicRecord;
+class Card;
+
+/**
+ @brief Pack of cards
+
+ A card pack consists of cards of certain card pattern.
+ A card pattern is a combination of fields: question field and answer fields.
+*/
+
+class CardPack: public TreeItem, public ICardPack
+{
+ Q_OBJECT
+
+public:
+ static const QString SynonymDelimiter;
+ static const QString HomonymDelimiter;
+
+public:
+ CardPack(IDictionary* dict);
+ CardPack(IDictionary* dict, const CardPack* otherPack);
+ ~CardPack();
+
+ // Getters
+
+ QString id() const;
+ QString name() const { return m_name; } // See also id()
+ QString curCardName() const { return m_curCardName; }
+ IDictionary* dictionary() const { return m_dictionary; }
+ QList<DicRecord*> getRecords() const;
+
+ // Tree view
+ const TreeItem* parent() const;
+ const TreeItem* child( int /*aRow*/ ) const { return NULL; }
+ int childCount() const { return 0; }
+ int columnCount() const { return 3; }
+ QVariant data( int aColumn ) const;
+ int row() const;
+ int topParentRow() const;
+
+ // Fields
+ QList<const Field*> getFields() const { return fields; }
+ const Field* getQuestionField() const;
+ QList<const Field*> getAnswerFields() const;
+ bool getUsesExactAnswer() const { return usesExactAnswer; }
+
+ // Cards
+ QStringList getCardQuestions() const { return cardQuestions; }
+ bool containsQuestion(const QString& question) const
+ { return cardQuestions.contains(question); }
+ int cardsNum() const { return cardQuestions.size(); }
+ Card* getCard(const QString& cardName);
+
+ // Study records
+ QList<StudyRecord> getStudyRecords() const { return studyRecords.values(); }
+ QList<QDateTime> getScheduledDates() const;
+ QList<StudyRecord> getStudyRecords(QString cardId) const
+ { return studyRecords.values(cardId); }
+ int studyRecordsNum() const { return studyRecords.uniqueKeys().size(); }
+ bool containsReviewedCards() const;
+ StudyRecord getStudyRecord(QString cardId) const;
+ QString findLastReviewedCard() const;
+
+ // Study statistics
+ QStringList getNewCards() const;
+ QStringList getActiveCards() const;
+ int getActiveRepeatingCardsNum() const;
+ int countScheduledForTodayCards() const;
+ QStringList getPriorityActiveCards() const;
+ QStringList getLearningCards() const;
+ int getLearningReviewsNum() const;
+ int getTimeToNextLearning() const;
+ int getInactiveLearningReviewsNum() const;
+ QStringList getSmallestIntervalCards(const QStringList& priorityCards);
+ int getTodayReviewedCardsNum() const;
+ int getTodayNewCardsNum() const;
+
+public:
+ // Setters
+ void setName( QString aName ) { m_name = aName; }
+ void setCurCard( const QString aCardId );
+
+ // Fields
+ void setFields( QList<const Field*> aFields ) { fields = aFields; }
+ void setField( int aPos, const Field* aField );
+ void addField( const Field* aField ) { fields << aField; }
+ void removeField( int aPos ) { fields.removeAt(aPos); }
+ void removeField( const Field* aField ) { fields.removeAll( aField ); }
+ void insertField( int aPos, const Field* aField ) { fields.insert( aPos, aField ); }
+ void setQstField( const Field* aField );
+ void setAnsFields( QList<const Field*> aFields );
+ void setUsesExactAnswer(bool uses) { usesExactAnswer = uses; }
+
+ void setReadingStudyFile(bool reading) { isReadingStudyFile = reading; }
+ void addStudyRecord( const QString aCardId, const StudyRecord& aStudyRecord );
+
+public:
+ void generateQuestions();
+ void disableDictRecordUpdates();
+ void enableDictRecordUpdates();
+
+private:
+ void destroyCards();
+ void addQuestionElementsForRecord(const DicRecord* record);
+ void removeAbsentCards(QStringList& cardQuestions);
+ void removeDelayedCard(const QString& cardName);
+
+signals:
+ void studyRecordAdded();
+ void cardsGenerated();
+
+private slots:
+ void processEntryChangedEvent( int aEntryIx, int aFieldIx = IDictionary::AllFields );
+
+private:
+ static const int MinRequiredFieldsNum = 2;
+
+private:
+ QString m_name; // Not used
+ IDictionary* m_dictionary;
+
+ // The first field is the question, others are answers.
+ QList<const Field*> fields;
+
+ QStringList cardQuestions; ///< Original order of questions
+ QHash<QString, Card*> cards; ///< Card name -> card (own)
+ QString m_curCardName;
+ QMultiHash<QString, StudyRecord> studyRecords; // Card ID -> Study record
+ bool isReadingStudyFile;
+ bool usesExactAnswer;
+};
+#endif
diff --git a/src/dictionary/DicCsvReader.cpp b/src/dictionary/DicCsvReader.cpp
new file mode 100644
index 0000000..ec80225
--- /dev/null
+++ b/src/dictionary/DicCsvReader.cpp
@@ -0,0 +1,205 @@
+#include "DicCsvReader.h"
+#include "Dictionary.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+
+DicCsvReader::DicCsvReader( Dictionary* aDict ):
+ m_dict( aDict )
+ {
+ }
+
+DicCsvReader::DicCsvReader():
+ m_dict( NULL )
+ {
+ }
+
+QFile::FileError DicCsvReader::readDict( const QString aCsvFilePath, const CsvImportData& aImportData )
+ {
+ if( !m_dict )
+ return QFile::NoError;
+
+ initData( aImportData );
+
+ // Erase dictionary content
+ m_dict->clearFieldPackConfig();
+ if( m_dict->entriesNum() > 0 )
+ m_dict->removeRecords( 0, m_dict->entriesNum() );
+
+ QFile file( aCsvFilePath );
+ if( !file.open( QIODevice::ReadOnly | QFile::Text ) ) // \r\n --> \n
+ return file.error();
+ QTextStream inStream( &file );
+ inStream.setCodec( m_params.textCodec );
+
+ // Ignore first rows
+ int rowNum = 1;
+ while( !inStream.atEnd() && rowNum++ < m_params.fromLine )
+ inStream.readLine();
+
+ int fieldsNum = readLines( inStream );
+ file.close();
+
+ // Add entries to dictionary
+ foreach( DicRecord* entry, m_entries )
+ m_dict->addRecord(entry);
+
+ // Name nameless fields and create dictionary fields
+ for( int i = 0; i < fieldsNum; i++ )
+ {
+ QString name = m_fieldNames.value( i );
+ if( name.isEmpty() )
+ name = QString::number( i+1 );
+ m_dict->addField( name );
+ }
+
+ // Create packs
+ CardPack* pack;
+ QList<const Field*> ansFields;
+
+ pack = new CardPack( m_dict );
+ pack->setQstField( m_dict->field(0) );
+ ansFields << m_dict->field(1);
+ for( int i = 2; i < m_dict->fieldsNum(); i++ )
+ ansFields << m_dict->field(i);
+ pack->setAnsFields( ansFields );
+ m_dict->addCardPack( pack );
+
+ ansFields.clear();
+ pack = new CardPack( m_dict );
+ pack->setQstField( m_dict->field(1) );
+ ansFields << m_dict->field(0);
+ for( int i = 2; i < m_dict->fieldsNum(); i++ )
+ ansFields << m_dict->field(i);
+ pack->setAnsFields( ansFields );
+ m_dict->addCardPack( pack );
+
+ return QFile::NoError;
+ }
+
+// The read field names are returned with fieldNames().
+
+QList<DicRecord*> DicCsvReader::readEntries( QString aCsvEntries, const CsvImportData& aImportData )
+ {
+ initData( aImportData );
+ QTextStream inStream( &aCsvEntries );
+ readLines( inStream );
+ if( !m_fieldNames.empty() )
+ return m_entries;
+ else
+ return QList<DicRecord*>(); /* If no required header, they are not real entries.
+ * When pasting, this text cannot be parsed and must be ignored. */
+ }
+
+void DicCsvReader::initData( const CsvImportData& aImportData )
+ {
+ m_params = aImportData;
+ // Construct regexp for the separators
+ QString fieldSepRxStr;
+ switch( m_params.fieldSeparationMode )
+ {
+ case EFieldSeparatorAnyCharacter:
+ fieldSepRxStr = QString("[") + m_params.fieldSeparators + "]";
+ break;
+ case EFieldSeparatorAnyCombination:
+ fieldSepRxStr = QString("[") + m_params.fieldSeparators + "]+";
+ break;
+ case EFieldSeparatorExactString:
+ fieldSepRxStr = m_params.fieldSeparators;
+ break;
+ }
+ m_fieldSepRx = QRegExp( fieldSepRxStr );
+ QChar delim = m_params.textDelimiter;
+ m_chunkEndRx = QRegExp( QString("(") + fieldSepRxStr + "|" + delim + ")" ); // End of text chunk
+ m_singleDelimiterRx = QRegExp( QString("([^") + delim + "]|^)" + delim + "([^" + delim + "]|$)" ); // Single text delimiter
+ }
+
+int DicCsvReader::readLines( QTextStream& aInStream )
+ {
+ QString line;
+
+ // Read field names from the header
+ m_fieldNames.clear();
+ if( m_params.firstLineIsHeader && !aInStream.atEnd() )
+ {
+ line = aInStream.readLine();
+ if( line.startsWith( m_params.commentChar ) )
+ line.remove( m_params.commentChar );
+ m_fieldNames = readFields( line.trimmed() );
+ }
+
+ // Read lines and create entries
+ int fieldsNum = 0;
+ m_entries.clear();
+ while( !aInStream.atEnd() )
+ {
+ line = aInStream.readLine();
+ QStringList fields = readFields( line.trimmed() );
+ DicRecord* entry = new DicRecord();
+ for( int i = 0; i < fields.size(); i++ )
+ {
+ QString name = m_fieldNames.value( i, QString::number( i+1 ) );
+ QString field = fields.value( i );
+ entry->setField( name, field );
+ }
+ m_entries << entry; // Add even empty entries (without fields)
+ if( fields.size() > fieldsNum )
+ fieldsNum = fields.size();
+ }
+ return fieldsNum;
+ }
+
+QStringList DicCsvReader::readFields( const QString aCsvLine )
+ {
+ QChar comment = m_params.commentChar;
+ if( !comment.isNull() && aCsvLine.startsWith( comment ) )
+ return QStringList(); // FUTURE FEATURE: read and mark as disabled
+
+ QChar delim = m_params.textDelimiter;
+ QStringList fields;
+ int curPos = 0;
+ QString curText;
+ while( curPos < aCsvLine.length() )
+ {
+ QChar curChar = aCsvLine[curPos];
+ if( curChar == delim ) // Text delimiter - Process the text until the next delimiter
+ {
+ int quoteEnd = aCsvLine.indexOf( m_singleDelimiterRx, curPos + 1 );
+ if( quoteEnd == -1) // End of line
+ quoteEnd = aCsvLine.length();
+ curText += aCsvLine.mid( curPos+1, quoteEnd-curPos );
+ curPos = quoteEnd + 2; // move beyond the delimiter
+ }
+ else if( m_fieldSepRx.indexIn( aCsvLine, curPos ) == curPos ) // Field separator - End of field
+ {
+ int sepLength = m_fieldSepRx.matchedLength();
+ curPos += sepLength;
+ fields << unescapeString( curText.trimmed() );
+ curText.clear();
+ }
+ else // Chunk of normal text. Process until next field separator or text delimiter.
+ {
+ int chunkEnd = aCsvLine.indexOf( m_chunkEndRx, curPos );
+ if( chunkEnd == -1 ) // End of line
+ chunkEnd = aCsvLine.length();
+ curText += aCsvLine.mid( curPos, chunkEnd-curPos );
+ curPos = chunkEnd;
+ }
+ }
+ if( !curText.isEmpty() ) // last Field
+ fields << unescapeString( curText.trimmed() );
+
+ if( m_params.colsToImport > 0 ) // Take only needed imported fields
+ fields = fields.mid( 0, m_params.colsToImport );
+
+ return fields;
+}
+
+/**
+ * Replaces double delimiters with one delimiter
+ */
+QString DicCsvReader::unescapeString( QString aString )
+{
+ QString delim = m_params.textDelimiter;
+ aString = aString.replace( delim + delim, delim );
+ return aString;
+}
diff --git a/src/dictionary/DicCsvReader.h b/src/dictionary/DicCsvReader.h
new file mode 100644
index 0000000..9e990dd
--- /dev/null
+++ b/src/dictionary/DicCsvReader.h
@@ -0,0 +1,41 @@
+#ifndef DICCSVREADER_H
+#define DICCSVREADER_H
+
+#include <QFile>
+#include <QString>
+#include <QStringList>
+#include <QRegExp>
+#include <QTextStream>
+
+#include "../export-import/CsvData.h"
+
+class Dictionary;
+class DicRecord;
+
+class DicCsvReader
+{
+public:
+ DicCsvReader( Dictionary* aDict );
+ DicCsvReader(); // For reading entries
+
+ QFile::FileError readDict( const QString aCsvFilePath, const CsvImportData& aImportData );
+ QList<DicRecord*> readEntries( QString aCsvEntries, const CsvImportData& aImportData );
+ QStringList fieldNames() { return m_fieldNames; } // Last read field names
+ QString unescapeString( QString aString );
+
+private:
+ void initData( const CsvImportData& aImportData );
+ int readLines( QTextStream& aInStream ); // Returns max number of fields
+ QStringList readFields( const QString aCsvLine );
+
+private:
+ Dictionary* m_dict; // Not own
+ CsvImportData m_params;
+ QRegExp m_fieldSepRx;
+ QRegExp m_chunkEndRx;
+ QRegExp m_singleDelimiterRx;
+ QStringList m_fieldNames;
+ QList<DicRecord*> m_entries; // Created, but ownership is transfered to the client
+};
+
+#endif // DICCSVREADER_H
diff --git a/src/dictionary/DicCsvWriter.cpp b/src/dictionary/DicCsvWriter.cpp
new file mode 100644
index 0000000..f2a5e2e
--- /dev/null
+++ b/src/dictionary/DicCsvWriter.cpp
@@ -0,0 +1,110 @@
+#include "DicCsvWriter.h"
+#include "Dictionary.h"
+#include "DicRecord.h"
+#include "Field.h"
+
+#include <QTextStream>
+
+DicCsvWriter::DicCsvWriter( const Dictionary* aDict ):
+ m_dict( aDict )
+ {
+ }
+
+DicCsvWriter::DicCsvWriter( const QList<DicRecord*> aEntries ):
+ m_dict( NULL ), m_entries( aEntries )
+ {
+ }
+
+QString DicCsvWriter::toCsvString( const CsvExportData& aExportData )
+ {
+ m_params = aExportData;
+ if( m_params.quoteAllFields )
+ m_fieldSepRegExp = QRegExp("."); // Any character
+ else
+ m_fieldSepRegExp = QRegExp( m_params.fieldSeparators ); // Exact string of separators
+
+ QChar delimiter = m_params.textDelimiter;
+ QString outStr;
+ QTextStream outStream( &outStr );
+
+ // Generate list of selected fields
+ m_selectedFieldNames.clear();
+ if( !m_dict ) // from entries
+ {
+ foreach(DicRecord* entry, m_entries )
+ foreach( QString fieldName, entry->getFields().keys() )
+ if( !m_selectedFieldNames.contains( fieldName ) )
+ m_selectedFieldNames << fieldName;
+ }
+ else // from dictionary
+ {
+ if( !m_params.usedCols.isEmpty() )
+ foreach( int col, m_params.usedCols )
+ m_selectedFieldNames << m_dict->field(col)->name();
+ else // All fields
+ foreach( Field* field, m_dict->fields() )
+ m_selectedFieldNames << field->name();
+
+ }
+
+ // Write column names
+ if( m_params.writeColumnNames )
+ {
+ QStringList escapedNames;
+ foreach( QString name, m_selectedFieldNames )
+ {
+ if( !delimiter.isNull() && name.contains( m_fieldSepRegExp ) )
+ name = delimiter + name + delimiter;
+ escapedNames << name;
+ }
+ QString header = QString( m_params.commentChar ) + " ";
+ header += escapedNames.join( m_params.fieldSeparators );
+ outStream << header << endl;
+ }
+
+ // For dictionary, copy entries into the local list.
+ if( m_dict )
+ m_entries = m_dict->getRecords();
+
+ // Write entries
+ bool lastLineWasEmpty = false;
+ for( int i = 0; i < m_entries.size(); i++ )
+ {
+ QString curLine = dicEntryToString( m_entries.value( i ) );
+ if( !(lastLineWasEmpty && curLine.isEmpty()) ) // Don't print several empty lines in a row
+ outStream << curLine << endl;
+ lastLineWasEmpty = curLine.isEmpty();
+ }
+ return outStr;
+ }
+
+QString DicCsvWriter::dicEntryToString(const DicRecord* aEntry ) const
+ {
+ if( !aEntry )
+ return QString();
+
+ QStringList fieldValues; // Convert the fields map into string list. If needed, delimit text ("" or ').
+ QChar delimiter = m_params.textDelimiter;
+ foreach( QString key, m_selectedFieldNames )
+ {
+ QString value = aEntry->getFieldValue( key );
+ /* If the field has embedded field separator or text delimiter (quote),
+ * it must be quoted. */
+ if( !delimiter.isNull() && ( value.contains( m_fieldSepRegExp ) || value.contains(delimiter) ) )
+ {
+ if( value.contains(delimiter) ) // Embedded text delimiter (")
+ value.replace( delimiter, QString(delimiter) + delimiter ); // Escape it with double delimiter ("")
+ value = delimiter + value + delimiter;
+ }
+ fieldValues << value;
+ }
+ // Remove all last empty fields
+ while( !fieldValues.isEmpty() && fieldValues.last().isEmpty() )
+ fieldValues.removeLast();
+ QString res = fieldValues.join( m_params.fieldSeparators );
+
+ /* FUTURE FEATURE: if( iIsCommented )
+ csv.insert( 0, m_params.iCommentChar ); // Insert the comment character */
+ return res;
+ }
+
diff --git a/src/dictionary/DicCsvWriter.h b/src/dictionary/DicCsvWriter.h
new file mode 100644
index 0000000..3764309
--- /dev/null
+++ b/src/dictionary/DicCsvWriter.h
@@ -0,0 +1,31 @@
+#ifndef DICCSVWRITER_H
+#define DICCSVWRITER_H
+
+#include <QRegExp>
+#include <QStringList>
+
+#include "../export-import/CsvData.h"
+
+class Dictionary;
+class DicRecord;
+
+class DicCsvWriter
+{
+public:
+ DicCsvWriter( const Dictionary* aDict ); // For writing from a dictionary
+ DicCsvWriter( const QList<DicRecord*> aEntries ); // For writing from list of entries
+
+ QString toCsvString( const CsvExportData& aExportData ); // Both for writing from a dictionary and list of entries
+
+private:
+ QString dicEntryToString( const DicRecord* aEntry ) const;
+
+private:
+ const Dictionary* m_dict;
+ QList<DicRecord*> m_entries; // Used both for dictionary and entries
+ QStringList m_selectedFieldNames;
+ CsvExportData m_params;
+ QRegExp m_fieldSepRegExp;
+};
+
+#endif // DICCSVWRITER_H
diff --git a/src/dictionary/DicRecord.cpp b/src/dictionary/DicRecord.cpp
new file mode 100644
index 0000000..91cebca
--- /dev/null
+++ b/src/dictionary/DicRecord.cpp
@@ -0,0 +1,62 @@
+#include "DicRecord.h"
+#include "Field.h"
+#include "ICardPack.h"
+
+DicRecord::DicRecord()
+ {
+ }
+
+DicRecord::DicRecord( const DicRecord& aOther ):
+ QObject(0)
+ {
+ fields = aOther.fields;
+ m_id04 = aOther.m_id04;
+ }
+
+void DicRecord::setField( QString aField, QString aValue )
+ {
+ fields[aField] = aValue;
+ emit valueChanged( aField );
+ }
+
+void DicRecord::renameField( const QString aOldFieldName, const QString aNewFieldName )
+ {
+ if( !fields.keys().contains( aOldFieldName ) )
+ return;
+ QString value = fields.value( aOldFieldName );
+ fields.remove( aOldFieldName );
+ fields[ aNewFieldName ] = value;
+}
+
+bool DicRecord::isValid(const QString& qstFieldName) const
+{
+ return !fields.value(qstFieldName).isEmpty() &&
+ hasNonEmptyAnswerField( qstFieldName );
+}
+
+bool DicRecord::hasNonEmptyAnswerField(const QString& qstFieldName) const
+{
+ foreach(QString name, fields.keys())
+ if(name != qstFieldName && !fields.value(name).isEmpty())
+ return true;
+ return false;
+}
+
+QStringList DicRecord::getFieldElements(const QString& fieldName) const
+{
+ QString value = fields.value(fieldName);
+ QStringList elements = value.split(ICardPack::SynonymDelimiter, QString::SkipEmptyParts);
+ QStringList trimmedElements;
+ foreach(QString element, elements)
+ trimmedElements << element.trimmed();
+ return trimmedElements;
+}
+
+QString DicRecord::getPreviewQuestionForPack(ICardPack* pack) const
+{
+ if(!pack)
+ return QString();
+ QString fieldName = pack->getQuestionField()->name();
+ QString questionFieldValue = getFieldValue(fieldName);
+ return questionFieldValue.split(ICardPack::SynonymDelimiter).first().trimmed();
+}
diff --git a/src/dictionary/DicRecord.h b/src/dictionary/DicRecord.h
new file mode 100644
index 0000000..913da08
--- /dev/null
+++ b/src/dictionary/DicRecord.h
@@ -0,0 +1,40 @@
+#ifndef DICENTRY_H
+#define DICENTRY_H
+
+#include <QtCore>
+
+class Field;
+class ICardPack;
+
+class DicRecord: public QObject
+{
+Q_OBJECT
+
+public:
+ DicRecord();
+ DicRecord( const DicRecord& aOther );
+
+ const QHash<QString, QString> getFields() const {return fields;}
+ QString getFieldValue(const QString& name) const { return fields.value(name); }
+ QStringList getFieldElements(const QString& fieldName) const;
+ QString getPreviewQuestionForPack(ICardPack* pack) const;
+ QString id04() const { return m_id04; }
+ void setField( QString aField, QString aValue );
+ void setId04( const QString& aId ) { m_id04 = aId; }
+ void renameField( const QString aOldFieldName, const QString aNewFieldName );
+ bool isValid(const QString& qstFieldName) const;
+
+private:
+ bool hasNonEmptyAnswerField(const QString& qstFieldName) const;
+
+signals:
+ void valueChanged( QString aField );
+
+private:
+ QHash<QString, QString> fields; // Field name -> field value
+ QString m_id04; // For v. 0.4
+};
+
+Q_DECLARE_METATYPE( DicRecord )
+
+#endif
diff --git a/src/dictionary/Dictionary.cpp b/src/dictionary/Dictionary.cpp
new file mode 100644
index 0000000..0779651
--- /dev/null
+++ b/src/dictionary/Dictionary.cpp
@@ -0,0 +1,601 @@
+#include "Dictionary.h"
+#include "CardPack.h"
+#include "../version.h"
+#include "DicRecord.h"
+#include "DictionaryWriter.h"
+#include "DictionaryReader.h"
+#include "../study/StudyFileWriter.h"
+#include "../study/StudyFileReader.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../main-view/AppModel.h"
+
+#include <QTextStream>
+#include <QStringList>
+#include <QtAlgorithms>
+#include <QDateTime>
+#include <QSettings>
+#include <QDir>
+
+const QString Dictionary::DictFileExtension(".fmd");
+const QString Dictionary::StudyFileExtension(".fms");
+const char* Dictionary::NoName( QT_TRANSLATE_NOOP("Dictionary", "noname") );
+
+Dictionary::Dictionary(const QString& aFilePath, bool aNameIsTemp, const AppModel* aAppModel):
+ IDictionary(aFilePath), m_appModel(aAppModel),
+ obsoleteId(QUuid()),
+ m_contentModified(true), m_studyModified(false), m_nameIsTemp(aNameIsTemp)
+ {
+ }
+
+Dictionary::~Dictionary()
+ {
+ foreach( Field* field, m_fields )
+ delete field;
+ foreach( CardPack* pack, m_cardPacks )
+ delete pack;
+ }
+
+const TreeItem* Dictionary::child( int aRow ) const
+ {
+ return cardPack( aRow );
+ }
+
+QVariant Dictionary::data( int aColumn ) const
+ {
+ if( aColumn == 0 )
+ return shortName( false );
+ else
+ return QVariant();
+ }
+
+int Dictionary::row() const
+ {
+ if( m_appModel )
+ return m_appModel->indexOfDictionary( const_cast<Dictionary*>(this) );
+ else
+ return 0;
+ }
+
+void Dictionary::clearFieldPackConfig()
+ {
+ while( !m_fields.isEmpty() )
+ delete m_fields.takeLast();
+ while( !m_cardPacks.isEmpty() )
+ delete m_cardPacks.takeLast();
+ }
+
+/** Copies from another dictionary:
+ * - fields, packs, study data.
+ * The old configuration is deleted.
+ * The dic records are not changed.
+ */
+void Dictionary::setDictConfig( const Dictionary* aOtherDic )
+ {
+ clearFieldPackConfig();
+
+ // Replace fields
+ foreach( Field* f, aOtherDic->fields() )
+ {
+ m_fields << new Field( f->name(), f->style() );
+ // Fix the renamed fields in the entries
+ // TODO: Refactor to remove the old name
+ foreach( DicRecord* entry, records )
+ entry->renameField( f->oldName(), f->name() );
+ }
+
+ // Replace card packs
+ foreach( CardPack* otherPack, aOtherDic->cardPacks() )
+ {
+ /* The packs are copied together with study data, because
+ * afterwards it's impossible to find correct modified pack
+ */
+ CardPack* newPack = new CardPack( this, otherPack );
+ addCardPack( newPack );
+ }
+
+ setContentModified();
+ generateCards();
+ }
+
+void Dictionary::setDefaultFields()
+{
+ m_fields << new Field( tr("Question"), FieldStyleFactory::DefaultStyle );
+ m_fields << new Field( tr("Answer"), FieldStyleFactory::DefaultStyle );
+ m_fields << new Field( tr("Example"), "Example" );
+
+ if( records.isEmpty() )
+ records << new DicRecord();
+
+ CardPack* pack;
+ QList<const Field*> ansFields;
+
+ // Question->Answer
+ pack = new CardPack( this );
+ pack->setQstField( m_fields[0] );
+ ansFields << m_fields[1] << m_fields[2];
+ pack->setAnsFields( ansFields );
+ addCardPack( pack );
+
+ // Answer->Question
+ ansFields.clear();
+ pack = new CardPack( this );
+ pack->setQstField( m_fields[1] );
+ ansFields << m_fields[0] << m_fields[2];
+ pack->setAnsFields( ansFields );
+ addCardPack( pack );
+}
+
+bool Dictionary::load(const QString filePath)
+{
+ if(!loadDictFile(filePath))
+ return false;
+ generateCards();
+ cleanObsoleteId();
+ QString studyFilePath = getStudyFilePath();
+ if(!QFile::exists(studyFilePath))
+ return true;
+ return loadStudyFile(studyFilePath);
+}
+
+bool Dictionary::loadDictFile(const QString filePath)
+{
+ cleanRecords();
+ QFile dicFile(filePath);
+ if(!dicFile.open(QIODevice::ReadOnly | QFile::Text))
+ {
+ errorMessage = tr("Cannot open dictionary file:") +
+ QString("<p>%1</p>").arg(QDir::toNativeSeparators(filePath));
+ return false;
+ }
+ this->filePath = filePath;
+ DictionaryReader dicReader(this);
+ bool ok = dicReader.read( &dicFile );
+ dicFile.close();
+ if(!ok)
+ {
+ errorMessage = dicReader.errorString();
+ return false;
+ }
+
+ if(m_contentModified)
+ {
+ if(save() != QFile::NoError)
+ return false;
+ }
+ return true;
+}
+
+void Dictionary::cleanRecords()
+{
+ while (!records.isEmpty())
+ delete records.takeFirst();
+}
+
+void Dictionary::cleanObsoleteId()
+{
+ if(!obsoleteId.isNull())
+ obsoleteId = QUuid();
+}
+
+bool Dictionary::loadStudyFile(const QString filePath)
+{
+ QFile studyFile(filePath);
+ if(!studyFile.open(QIODevice::ReadOnly | QFile::Text))
+ {
+ errorMessage = tr("Cannot open study file:") +
+ QString("<p>%1</p>").arg(QDir::toNativeSeparators(filePath));
+ return false;
+ }
+ StudyFileReader studyReader( this );
+ bool ok = studyReader.read( &studyFile );
+ studyFile.close();
+ if(!ok)
+ errorMessage = studyReader.errorString() + QString(" at %1:%2")
+ .arg( studyReader.lineNumber() )
+ .arg( studyReader.columnNumber() );
+
+ if(m_studyModified)
+ {
+ if(saveStudy() != QFile::NoError)
+ return false;
+ }
+ return true;
+}
+
+QFile::FileError Dictionary::save( const QString aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = saveContent( aFilePath );
+ if(error != QFile::NoError)
+ return error;
+
+ if(aChangeFilePath && aFilePath != filePath)
+ {
+ filePath = aFilePath;
+ m_studyModified = true;
+ emit filePathChanged();
+ }
+
+ if(m_studyModified)
+ {
+ error = saveStudy();
+ if(error != QFile::NoError)
+ return error;
+ }
+
+ return QFile::NoError;
+}
+
+QFile::FileError Dictionary::saveContent( const QString aFilePath )
+ {
+ QFile file( aFilePath );
+ if( !file.open( QIODevice::WriteOnly | QFile::Text ) )
+ return file.error();
+ DictionaryWriter writer( this );
+ writer.write( &file );
+ file.close();
+ setContentModified( false );
+ m_nameIsTemp = false;
+ return QFile::NoError;
+ }
+
+QFile::FileError Dictionary::saveStudy()
+ {
+ if(!m_studyModified)
+ return QFile::NoError;
+ QFile file( getStudyFilePath() );
+ if(!file.open(QIODevice::WriteOnly | QFile::Text))
+ return file.error();
+ StudyFileWriter writer(this);
+ writer.write(&file);
+ file.close();
+ setStudyModified(false);
+ return QFile::NoError;
+ }
+
+QString Dictionary::shortName( bool aMarkModified ) const
+{
+QString fileName;
+if( !filePath.isEmpty() )
+ fileName = QFileInfo(filePath).fileName();
+else
+ fileName = tr(NoName) + DictFileExtension;
+if(aMarkModified && m_contentModified)
+ fileName += "*";
+return fileName;
+}
+
+QString Dictionary::getStudyFilePath() const
+{
+QString studyFilePath;
+if(obsoleteId.isNull())
+ {
+ // Take study file from the same dictionary directory
+ QFileInfo fileInfo(filePath);
+ studyFilePath = fileInfo.path() + "/" + fileInfo.completeBaseName() + StudyFileExtension;
+ }
+else
+ {
+ // Old dictionary. Take study file from the user settings directory
+ QSettings settings;
+ QFileInfo settingsInfo( settings.fileName() );
+ studyFilePath = settingsInfo.path() + "/study/" + obsoleteId.toString() + StudyFileExtension;
+ }
+return studyFilePath;
+}
+
+void Dictionary::setContentModified( bool aModified )
+{
+if( aModified != m_contentModified ) // The Content Modified state is changed
+ {
+ m_contentModified = aModified;
+ emit contentModifiedChanged( m_contentModified );
+ }
+}
+
+void Dictionary::setStudyModified(bool aModified)
+{
+if(aModified != m_studyModified)
+ {
+ m_studyModified = aModified;
+ emit studyModifiedChanged(m_studyModified);
+ }
+}
+
+const DicRecord* Dictionary::getRecord(int aIndex) const
+{
+ return records.value( aIndex );
+}
+
+const DicRecord* Dictionary::entry04( const QString aId04 ) const
+ {
+ if( aId04.isEmpty() )
+ return NULL;
+ foreach( DicRecord* entry, records )
+ if( entry->id04() == aId04 )
+ return entry;
+ return NULL;
+ }
+
+void Dictionary::setFieldValue( int aEntryIx, int aFieldIx, QString aValue )
+ {
+ DicRecord* entry = records.value(aEntryIx);
+ if( !entry )
+ return;
+ const Field* field = m_fields[aFieldIx];
+ if( field->name().isNull() )
+ return;
+ entry->setField( field->name(), aValue );
+ setContentModified();
+ emit entryChanged( aEntryIx, aFieldIx );
+ }
+
+ /// @return -1, if the given ID 0.4 was not found
+int Dictionary::fieldId04ToIx( const QString aId ) const
+ {
+ if( aId.isEmpty() )
+ return -1;
+ int i=0;
+ foreach( Field* f, m_fields )
+ {
+ if( f->id04() == aId )
+ return i;
+ i++;
+ }
+ return -1;
+ }
+
+const Field* Dictionary::field( const QString aFieldName ) const
+ {
+ foreach( Field* f, m_fields )
+ if( f->name() == aFieldName )
+ return f;
+ return NULL;
+ }
+
+QStringList Dictionary::fieldNames() const
+ {
+ QStringList names;
+ foreach( Field* f, m_fields )
+ names << f->name();
+ return names;
+ }
+
+CardPack* Dictionary::cardPack( QString aId ) const
+ {
+ foreach( CardPack* pack, m_cardPacks )
+ {
+ if( pack->id() == aId )
+ return pack;
+ }
+ return NULL;
+ }
+
+void Dictionary::setFieldName( int aField, QString aName )
+ {
+ if( aField >= m_fields.size() )
+ return;
+ m_fields[aField]->setName(aName);
+ setContentModified();
+ emit fieldChanged( aField );
+ }
+
+void Dictionary::setFieldStyle( int aField, QString aStyle )
+ {
+ if( aField >= m_fields.size() )
+ return;
+ m_fields[aField]->setStyle(aStyle);
+ setContentModified();
+ emit fieldChanged( aField );
+ }
+
+void Dictionary::insertField( int aPos, QString aName )
+ {
+ if( aPos > m_fields.size() )
+ return;
+ m_fields.insert( aPos, new Field(aName) );
+ setContentModified();
+ emit fieldInserted( aPos );
+ }
+
+void Dictionary::insertField( int aPos, Field* aFieldPtr )
+ {
+ if( aPos > m_fields.size() )
+ return;
+ m_fields.insert( aPos, aFieldPtr );
+ setContentModified();
+ emit fieldInserted( aPos );
+ }
+
+void Dictionary::addField( QString aName, QString aStyle )
+ {
+ m_fields << new Field( aName, aStyle );
+ setContentModified();
+ emit fieldInserted( m_fields.size()-1 );
+ }
+
+// TODO: Make undo command
+void Dictionary::addFields( QStringList aFieldNames )
+ {
+ foreach( QString aName, aFieldNames )
+ addField( aName );
+ }
+
+/// Just removes field pointer from list
+void Dictionary::removeField( int aPos )
+ {
+ if( aPos >= m_fields.size() )
+ return;
+ m_fields.removeAt( aPos );
+ setContentModified();
+ emit fieldRemoved( aPos );
+ }
+
+/// Removes the field pointer and destroys the field itself!
+void Dictionary::destroyField( int aPos )
+ {
+ if( aPos >= m_fields.size() )
+ return;
+
+ Field* removedField = m_fields.takeAt( aPos );
+ setContentModified();
+
+ // Remove this field in all packs
+ foreach( CardPack* pack, m_cardPacks )
+ pack->removeField( removedField );
+
+ delete removedField;
+ emit fieldRemoved( aPos );
+ emit fieldDestroyed( removedField );
+ }
+
+void Dictionary::insertPack( int aPos, CardPack* aPack )
+ {
+ if( aPos > m_cardPacks.size() )
+ return;
+ m_cardPacks.insert( aPos, aPack );
+ connect( aPack, SIGNAL(studyRecordAdded()), SLOT(setStudyModified()) );
+ connect( aPack, SIGNAL(cardsGenerated()), SIGNAL(cardsGenerated()) );
+ emit packInserted( aPos );
+ }
+
+
+void Dictionary::addCardPack( CardPack* aCardPack )
+ {
+ insertPack( m_cardPacks.size(), aCardPack );
+ }
+
+/// Just removes pack pointer from list
+void Dictionary::removePack( int aPos )
+ {
+ if( aPos >= m_cardPacks.size() )
+ return;
+ m_cardPacks.removeAt( aPos );
+ emit packRemoved( aPos );
+ }
+
+/// Removes the field pointer and destroys the field itself!
+void Dictionary::destroyPack( int aPos )
+ {
+ if( aPos >= m_cardPacks.size() )
+ return;
+ CardPack* removedPack = m_cardPacks.takeAt( aPos );
+ delete removedPack;
+ emit packRemoved( aPos );
+ emit packDestroyed( removedPack );
+ }
+
+void Dictionary::setRecord( int aIndex, const DicRecord& aRecord )
+ {
+ if( aIndex < 0 || aIndex >= records.size() )
+ return;
+ DicRecord* newRecord = new DicRecord( aRecord );
+ delete records.value( aIndex ); // delete old record
+ records.replace( aIndex, newRecord );
+ setContentModified();
+ emit entryChanged( aIndex, AllFields );
+ }
+
+void Dictionary::insertEntry(int aIndex, DicRecord* aEntry)
+ {
+ records.insert( aIndex, aEntry );
+ notifyRecordsInserted(aIndex, 1);
+ }
+
+void Dictionary::insertEntries( int aIndex, int aNum )
+ {
+ for(int i=0; i < aNum; i++)
+ records.insert( aIndex + i, new DicRecord() );
+ notifyRecordsInserted(aIndex, aNum);
+ }
+
+void Dictionary::insertEntries( int aIndex, QList<DicRecord*> aEntries )
+ {
+ int i = 0;
+ foreach( DicRecord* entry, aEntries )
+ {
+ records.insert( aIndex + i, entry );
+ i++;
+ }
+ notifyRecordsInserted(aIndex, aEntries.size());
+ }
+
+void Dictionary::notifyRecordsInserted(int index, int num)
+{
+ setContentModified();
+ emit entriesInserted(index, num);
+}
+
+void Dictionary::removeRecords(int aIndex, int aNum)
+ {
+ Q_ASSERT( aIndex + aNum <= records.size() );
+ for( int i=0; i < aNum; i++ )
+ delete records.takeAt( aIndex );
+ setContentModified();
+ emit entriesRemoved( aIndex, aNum );
+ }
+
+void Dictionary::removeRecord( QString aQuestion )
+ {
+ QMutableListIterator<DicRecord*> it( records );
+ int i = 0;
+ int removedIndex = -1; // First removed record
+ while( it.hasNext() )
+ {
+ DicRecord* record = it.next();
+ foreach( QString fieldStr, record->getFields() )
+ {
+ QStringList elements = fieldStr.split( CardPack::SynonymDelimiter, QString::SkipEmptyParts );
+ if( elements.contains( aQuestion ) )
+ {
+ it.remove();
+ if( removedIndex < 0 )
+ removedIndex = i;
+ break;
+ }
+ }
+ i++;
+ }
+ if( removedIndex >= 0 ) // if something was removed
+ {
+ setContentModified();
+ emit entriesRemoved( removedIndex, 1 );
+ }
+ }
+
+void Dictionary::generateCards()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->generateQuestions();
+ }
+
+void Dictionary::disableRecordUpdates()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->disableDictRecordUpdates();
+ }
+
+void Dictionary::enableRecordUpdates()
+ {
+ if( m_cardPacks.isEmpty() )
+ return;
+ foreach( CardPack* pack, m_cardPacks )
+ pack->enableDictRecordUpdates();
+ generateCards();
+ }
+
+QString Dictionary::shortenImagePaths(QString text) const
+{
+ QRegExp imgRx(QString("(<img\\s+src=\")%1([/\\\\])").arg(getImagesPath()));
+ text.replace(imgRx, "\\1%%\\2");
+ return text;
+}
+
+QString Dictionary::getFieldValue(int recordNum, int fieldNum) const
+{
+ QString fieldName = field(fieldNum)->name();
+ return getRecord(recordNum)-> getFieldValue(fieldName);
+}
diff --git a/src/dictionary/Dictionary.h b/src/dictionary/Dictionary.h
new file mode 100644
index 0000000..5cbeaa2
--- /dev/null
+++ b/src/dictionary/Dictionary.h
@@ -0,0 +1,187 @@
+#ifndef DICTIONARY_H
+#define DICTIONARY_H
+
+#include <QObject>
+#include <QList>
+#include <QString>
+#include <QFileInfo>
+#include <QFile>
+#include <QPair>
+#include <QUuid>
+
+#include "../export-import/CsvData.h"
+#include "IDictionary.h"
+#include "Card.h"
+#include "Field.h"
+#include "TreeItem.h"
+
+class DicRecord;
+class CardPack;
+class AppModel;
+
+/** \brief The dictionary
+ *
+ * The dictionary consists of entries #iEntries.
+ * The property #iIsContentModified shows if the dictionary contents are modified and not saved. Different modifications are possible:
+ * \li A single card was modified. Means contents of a card were modified. Signal CardModified(int) is emitted.
+ * \li The Content Modified state is changed. Means that iIsContentModified has changed. Signal ContentModifiedChanged(bool) is emitted.
+ * \li Some cards were inserted or removed. Independently of whether the cards are valid, signal
+ * CardsInserted(int, int) or CardsRemoved(int,int) is emitted.
+ * \li The file path is modified. Means that the dictionary file was renamed, e.g. after "Save as" operation.
+ * Signal filePathChanged() is emitted.
+ *
+ * All card-modifying functions and signals call SetModified().
+ */
+
+class Dictionary: public TreeItem, public IDictionary
+{
+Q_OBJECT
+
+friend class DictionaryReader;
+
+public:
+
+ // Methods
+ Dictionary( const QString& aFilePath = "", bool aNameIsTemp = false, const AppModel* aAppModel = NULL );
+ ~Dictionary();
+
+ const TreeItem* parent() const { return NULL; }
+ const TreeItem* child( int aRow ) const;
+ int childCount() const { return cardPacksNum(); }
+ int columnCount() const { return 1; }
+ QVariant data( int aColumn ) const;
+ int row() const;
+ int topParentRow() const { return row(); }
+
+ void clearFieldPackConfig();
+ void setDictConfig( const Dictionary* aOtherDic );
+ void setDefaultFields();
+ bool load(const QString aFilePath);
+ QFile::FileError save(const QString aFilePath, bool aChangeFilePath = true );
+ QFile::FileError save() { return save( filePath ); }
+ QFile::FileError saveContent( const QString aFilePath );
+ QFile::FileError saveStudy();
+
+ QString getFilePath() const { return filePath; }
+ QString shortName( bool aMarkModified = true ) const;
+ QString getStudyFilePath() const;
+ QString getErrorMessage() const { return errorMessage; }
+ bool contentModified() const { return m_contentModified; }
+ bool studyModified() const { return m_studyModified; }
+ bool nameIsTemp() const { return m_nameIsTemp; }
+ bool empty() const { return records.isEmpty(); }
+
+ QList<Field*> fields() const { return m_fields; }
+ const Field* field( int aIx ) const { return m_fields.value( aIx ); }
+ const Field* field( const QString aFieldName ) const;
+ QString getFieldValue(int recordNum, int fieldNum) const;
+ int fieldsNum() const { return m_fields.size(); }
+ QStringList fieldNames() const;
+
+ int cardPacksNum() const { return m_cardPacks.size(); }
+ QList<CardPack*> cardPacks() const { return m_cardPacks; }
+ CardPack* cardPack( int aIx ) const {return m_cardPacks.value(aIx);}
+ CardPack* cardPack( QString aId ) const;
+ int indexOfCardPack( CardPack* aPack ) const { return m_cardPacks.indexOf( aPack ); }
+
+ int fieldId04ToIx( const QString aId ) const; // Obsolete
+ const DicRecord* getRecord( int aIndex ) const;
+ const DicRecord* entry04( const QString aId04 ) const; // Obsolete
+ const DicRecord* operator[]( int aIndex ) const { return getRecord(aIndex); }
+ int entriesNum() const { return records.size(); }
+ int indexOfRecord( DicRecord* aRecord ) const { return records.indexOf( aRecord ); }
+
+ void setAppModel( const AppModel* aModel ) { Q_ASSERT( !m_appModel ); m_appModel = aModel; }
+
+ // Modify fields
+
+ void setFieldName( int aField, QString aName );
+ void setFieldStyle( int aField, QString aStyle );
+ void insertField( int aPos, QString aName );
+ void insertField( int aPos, Field* aFieldPtr );
+ void addField( QString aName, QString aStyle = "" );
+ void addFields( QStringList aFieldNames );
+ void removeField( int aPos );
+ void destroyField( int aPos );
+
+ // Modify card packs
+
+ void insertPack( int aPos, CardPack* aPack );
+ void removePack( int aPos );
+ void destroyPack( int aPos );
+ void addCardPack( CardPack* aCardPack );
+
+ // Modify records
+
+ void setFieldValue( int aEntryIx, int aFieldIx, QString aValue );
+ void setRecord( int aIndex, const DicRecord& aRecord );
+ void insertEntry(int aIndex, DicRecord* aEntry);
+ void insertEntries(int aIndex, int aNum);
+ void insertEntries( int aIndex, QList<DicRecord*> aEntries );
+ void removeEntry(int aIndex) { removeRecords( aIndex, 1 ); }
+ void cleanRecords();
+ void removeRecords(int aIndex, int aNum);
+ void removeRecord(QString aQuestion);
+ void disableRecordUpdates();
+ void enableRecordUpdates();
+
+ QString shortenImagePaths(QString text) const;
+
+signals:
+ void entryChanged( int aEntryIx, int aFieldIx );
+ void entriesRemoved( int aIndex, int aNum );
+ void entriesInserted( int aIndex, int aNum );
+
+ void fieldChanged( int aField );
+ void fieldInserted( int aField );
+ void fieldRemoved( int aField );
+ void fieldDestroyed( Field* aField );
+
+ void packInserted( int aPack );
+ void packRemoved( int aPack );
+ void packDestroyed( CardPack* aPack );
+ void contentModifiedChanged( bool aModified );
+ void studyModifiedChanged( bool aModified );
+
+ void filePathChanged();
+ void cardsGenerated();
+
+public slots:
+ void setContentModified( bool aModified = true );
+ void setContentClean( bool aClean = true ) { setContentModified( !aClean ); }
+ void setStudyModified( bool aModified = true );
+ void generateCards();
+
+private:
+ bool loadDictFile(const QString filePath);
+ bool loadStudyFile(const QString filePath);
+ void cleanObsoleteId();
+
+protected:
+ void notifyRecordsInserted(int index, int num);
+
+public:
+ static const char* NoName;
+ static const QString DictFileExtension;
+ static const QString StudyFileExtension;
+
+private:
+ const AppModel* m_appModel;
+ QUuid obsoleteId; // Obsolete starting from 1.0
+ QList<Field*> m_fields;
+
+ bool m_contentModified;
+ bool m_studyModified;
+
+ /**
+ The current file name is temporary and must be changed at the next save.
+ Use cases:
+ \li the dictionary has just been imported from a CSV file
+ \li the dictionary of obsolete format was converted to the modern format
+ */
+ bool m_nameIsTemp;
+
+ QString errorMessage;
+};
+
+#endif
diff --git a/src/dictionary/DictionaryReader.cpp b/src/dictionary/DictionaryReader.cpp
new file mode 100644
index 0000000..8a3391c
--- /dev/null
+++ b/src/dictionary/DictionaryReader.cpp
@@ -0,0 +1,384 @@
+#include "DictionaryReader.h"
+#include "Dictionary.h"
+#include "../version.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QUuid>
+#include <QMessageBox>
+
+const QString DictionaryReader::MinSupportedDictVersion = "0.3";
+
+DictionaryReader::DictionaryReader( Dictionary* aDict ):
+ m_dict( aDict )
+ {
+
+ }
+
+bool DictionaryReader::read( QIODevice* aDevice )
+ {
+ setDevice( aDevice );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isStartElement() )
+ {
+ if( name() == "dict" )
+ readDict();
+ else
+ raiseError( Dictionary::tr("The file is not a dictionary file.") );
+ }
+ }
+ return !error();
+ }
+
+void DictionaryReader::readDict()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict" );
+
+ m_dict->m_contentModified = false;
+ QString id = attributes().value( "id" ).toString();
+ if(!id.isEmpty()) // Id is obsolete. If it exists, write it down just for loading the study file.
+ m_dict->obsoleteId = QUuid(id);
+
+ m_dictVersion = attributes().value( "version" ).toString();
+ if( m_dictVersion == DIC_VERSION)
+ readDictCurrentVersion();
+ else if( m_dictVersion == "1.0" )
+ {
+ notifyObsoleteVersion(m_dictVersion);
+ readDictCurrentVersion();
+ }
+ else if( m_dictVersion == "0.4" )
+ {
+ notifyObsoleteVersion( m_dictVersion );
+ readDict04();
+ }
+ else if( m_dictVersion == "0.3" )
+ {
+ notifyObsoleteVersion( m_dictVersion );
+ readDict03();
+ }
+ else
+ {
+ QMessageBox::warning( NULL, Dictionary::tr("Unsupported format"),
+ Dictionary::tr("Dictionary uses unsupported format %1.\n"
+ "The minimum supported version is %2" )
+ .arg( m_dictVersion )
+ .arg( MinSupportedDictVersion ) );
+ }
+ }
+
+void DictionaryReader::notifyObsoleteVersion( const QString& aOldVersion )
+ {
+ QMessageBox::warning( NULL, Dictionary::tr("Old dictionary"),
+ Dictionary::tr("Dictionary %1 uses obsolete format %2.\n"
+ "It will be converted to the current format %3." )
+ .arg(m_dict->shortName(false))
+ .arg(aOldVersion).arg(DIC_VERSION));
+ m_dict->m_contentModified = true;
+ }
+
+void DictionaryReader::readUnknownElement()
+ {
+ Q_ASSERT( isStartElement() );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ readUnknownElement();
+ }
+ }
+
+void DictionaryReader::readDictCurrentVersion()
+ {
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "fields" )
+ readFields();
+ else if( name() == "packs" )
+ readPacks();
+ else if( name() == "entries" )
+ readEntries();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readFields()
+ {
+ Q_ASSERT( isStartElement() && name() == "fields" );
+
+ if( m_dictVersion == "0.4" )
+ {
+ m_curCardPack = new CardPack( m_dict );
+ m_curAnsFieldList.clear();
+ }
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "field" )
+ readField();
+ else
+ readUnknownElement();
+ }
+ }
+
+ if( m_dictVersion == "0.4" && m_curCardPack )
+ {
+ m_curCardPack->setAnsFields( m_curAnsFieldList );
+ m_dict->m_cardPacks << m_curCardPack;
+
+ // And create the reversed pack: qst and ans swapped
+ CardPack* pack2 = new CardPack( m_dict );
+ pack2->setQstField( m_curCardPack->getAnswerFields()[0] );
+ QList<const Field*> fList2;
+ fList2 << m_curCardPack->getQuestionField(); // First answer
+ for( int i=1; i<m_curCardPack->getAnswerFields().size(); i++ ) // Other answers
+ fList2 << m_curCardPack->getAnswerFields()[i];
+ pack2->setAnsFields( fList2 );
+ m_dict->m_cardPacks << pack2;
+ }
+ }
+
+void DictionaryReader::readField()
+ {
+ Q_ASSERT( isStartElement() && name() == "field" );
+
+ QString qstAttr;
+ QString ansAttr;
+ QString id04Attr;
+ if( m_dictVersion == "0.4" )
+ {
+ qstAttr = attributes().value("question").toString();
+ ansAttr = attributes().value("answer").toString();
+ id04Attr = attributes().value("id").toString();
+ }
+
+ QString style = attributes().value("style").toString();
+ if( style.isEmpty() )
+ style = FieldStyleFactory::DefaultStyle;
+ QString fieldName = readElementText().trimmed();
+ Field* field = new Field( fieldName, style );
+
+ m_dict->m_fields << field; // insert in order of occurence
+
+ if( m_dictVersion == "0.4" )
+ {
+ Field* field = m_dict->m_fields.last();
+ if( !id04Attr.isEmpty() )
+ field->setId04( id04Attr );
+ if( qstAttr == "yes" && m_curCardPack )
+ m_curCardPack->setQstField( field );
+ else
+ {
+ bool ok;
+ int ansIx = ansAttr.toInt( &ok );
+ if( ok )
+ m_curAnsFieldList.insert( ansIx, field );
+ }
+ }
+
+ }
+
+void DictionaryReader::readPacks()
+ {
+ Q_ASSERT( isStartElement() && name() == "packs" );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "pack" )
+ readPack();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readPack()
+ {
+ Q_ASSERT( isStartElement() && name() == "pack" );
+ CardPack* cardPack = new CardPack( m_dict );
+ QString exactAnswer = attributes().value("exact-ans").toString();
+ if(exactAnswer == "true")
+ cardPack->setUsesExactAnswer(true);
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "qst" || name() == "ans" )
+ {
+ QString fieldName = readElementText().trimmed();
+ const Field* field = m_dict->field( fieldName );
+ if( !field )
+ continue;
+ if( name() == "qst" )
+ cardPack->setQstField( field );
+ else // ans
+ cardPack->addField( field );
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->addCardPack( cardPack );
+ }
+
+void DictionaryReader::readEntries()
+ {
+ Q_ASSERT( isStartElement() && name() == "entries" );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "e" )
+ readE();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+/** Read entry */
+void DictionaryReader::readE()
+ {
+ DicRecord* dicEntry = new DicRecord;
+ if( m_dictVersion == "0.4" )
+ {
+ QString idStr = attributes().value("id").toString();
+ dicEntry->setId04( idStr );
+ }
+
+ int curFieldIx = 0;
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "f" )
+ {
+ if( m_dictVersion == "0.4" )
+ {
+ QString fieldId04 = attributes().value("id").toString();
+ int ix = m_dict->fieldId04ToIx( fieldId04 );
+ if( ix > -1 ) // found
+ curFieldIx = ix;
+ }
+ QString fieldValue = readElementText().trimmed();
+ const Field* field = m_dict->field( curFieldIx );
+ if( !field )
+ break; // no more fields
+ dicEntry->setField( field->name(), fieldValue );
+ curFieldIx++;
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->records << dicEntry;
+ }
+
+void DictionaryReader::readDict04()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict"
+ && attributes().value("version") == "0.4" );
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "fields" )
+ readFields();
+ else if( name() == "c" )
+ readE();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void DictionaryReader::readDict03()
+ {
+ Q_ASSERT( isStartElement() && name() == "dict"
+ && attributes().value("version") == "0.3" );
+
+ // Read entries
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "card" )
+ readE03();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+/** Read entry v.0.3*/
+void DictionaryReader::readE03()
+ {
+ Q_ASSERT( isStartElement() && m_dictVersion == "0.3" && name() == "card" );
+ DicRecord* dicEntry = new DicRecord;
+
+ QMap<QString, QString> fieldNames; // tag name, field name
+ fieldNames["qst"] = Dictionary::tr("Question");
+ fieldNames["ans"] = Dictionary::tr("Answer");
+ fieldNames["xmp"] = Dictionary::tr("Example");
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ QString tagName = name().toString();
+ if( fieldNames.contains( tagName ) )
+ {
+ QString fieldValue = readElementText().trimmed();
+ dicEntry->setField( fieldNames.value( tagName ), fieldValue );
+ }
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_dict->records << dicEntry;
+ }
diff --git a/src/dictionary/DictionaryReader.h b/src/dictionary/DictionaryReader.h
new file mode 100644
index 0000000..ca2ff00
--- /dev/null
+++ b/src/dictionary/DictionaryReader.h
@@ -0,0 +1,44 @@
+#ifndef DICTIONARYREADER_H
+#define DICTIONARYREADER_H
+
+#include <QXmlStreamReader>
+#include <QList>
+#include <QString>
+
+class Dictionary;
+class CardPack;
+class Field;
+
+class DictionaryReader : public QXmlStreamReader
+{
+public:
+ DictionaryReader( Dictionary* aDict );
+ bool read( QIODevice* aDevice );
+
+private:
+ static const QString MinSupportedDictVersion;
+
+private:
+ void readDict();
+ void notifyObsoleteVersion( const QString& aOldVersion );
+ void readUnknownElement();
+ void readDictCurrentVersion();
+ void readFields();
+ void readField();
+ void readPacks();
+ void readPack();
+ void readEntries();
+ void readE();
+ void readDict04();
+ void readDict03();
+ void readE03();
+
+private:
+ Dictionary* m_dict;
+ QString m_dictVersion;
+ CardPack* m_curCardPack; ///< For 0.4
+ QList<const Field*> m_curAnsFieldList; ///< For 0.4
+
+};
+
+#endif // DICTIONARYREADER_H
diff --git a/src/dictionary/DictionaryWriter.cpp b/src/dictionary/DictionaryWriter.cpp
new file mode 100644
index 0000000..14cd087
--- /dev/null
+++ b/src/dictionary/DictionaryWriter.cpp
@@ -0,0 +1,79 @@
+#include "DictionaryWriter.h"
+#include "Dictionary.h"
+#include "../version.h"
+#include "CardPack.h"
+#include "DicRecord.h"
+
+DictionaryWriter::DictionaryWriter( const Dictionary* aDict ):
+ m_dict( aDict )
+{
+ setAutoFormatting( true );
+}
+
+bool DictionaryWriter::write( QIODevice* aDevice )
+{
+ setDevice( aDevice );
+ writeStartDocument();
+ writeDTD( "<!DOCTYPE freshmemory-dict>" );
+ writeStartElement("dict");
+ writeAttribute( "version", DIC_VERSION );
+
+ writeStartElement( "fields" );
+ foreach( Field *field, m_dict->fields() )
+ writeField( field );
+ writeEndElement();
+
+ writeStartElement( "packs" );
+ foreach( const CardPack* pack, m_dict->cardPacks() )
+ writePack( pack );
+ writeEndElement();
+
+ writeStartElement( "entries" );
+ foreach( const DicRecord* entry, m_dict->getRecords() )
+ writeEntry( entry, m_dict->fields() );
+ writeEndElement();
+
+ writeEndDocument();
+ return true;
+}
+
+void DictionaryWriter::writeField( Field* aField )
+ {
+ writeStartElement( "field" );
+ QString style = aField->style();
+ if( !style.isEmpty() && style != FieldStyleFactory::DefaultStyle )
+ writeAttribute( "style", style );
+ writeCharacters( aField->name() );
+ writeEndElement();
+ }
+
+void DictionaryWriter::writePack( const CardPack* aPack )
+ {
+ writeStartElement( "pack" );
+ if(aPack->getUsesExactAnswer())
+ writeAttribute( "exact-ans", "true" );
+ writeTextElement( "qst", aPack->getQuestionField()->name() );
+ foreach( const Field* field, aPack->getAnswerFields() )
+ writeTextElement( "ans", field->name() );
+ writeEndElement();
+ }
+
+void DictionaryWriter::writeEntry( const DicRecord* aEntry, QList<Field*> aFields )
+ {
+ writeStartElement( "e" );
+ int emptyFieldsNum = 0;
+ foreach( Field* field, aFields )
+ {
+ QString fieldValue = aEntry->getFieldValue( field->name() );
+ if( fieldValue.isEmpty() )
+ emptyFieldsNum++;
+ else
+ {
+ for( int k = 0; k < emptyFieldsNum; k++ )
+ writeEmptyElement( "f" );
+ emptyFieldsNum = 0;
+ writeTextElement( "f", fieldValue );
+ }
+ }
+ writeEndElement();
+ }
diff --git a/src/dictionary/DictionaryWriter.h b/src/dictionary/DictionaryWriter.h
new file mode 100644
index 0000000..82050a4
--- /dev/null
+++ b/src/dictionary/DictionaryWriter.h
@@ -0,0 +1,26 @@
+#ifndef DICTIONARYWRITER_H
+#define DICTIONARYWRITER_H
+
+#include <QXmlStreamWriter>
+
+class Dictionary;
+class Field;
+class CardPack;
+class DicRecord;
+
+class DictionaryWriter : public QXmlStreamWriter
+{
+public:
+ DictionaryWriter( const Dictionary* aDict );
+ bool write( QIODevice* aDevice );
+
+private:
+ void writeField( Field* aField );
+ void writePack( const CardPack* aPack );
+ void writeEntry( const DicRecord* aEntry, QList<Field*> aFields );
+
+private:
+ const Dictionary* m_dict;
+};
+
+#endif // DICTIONARYWRITER_H
diff --git a/src/dictionary/Field.cpp b/src/dictionary/Field.cpp
new file mode 100644
index 0000000..5992ac0
--- /dev/null
+++ b/src/dictionary/Field.cpp
@@ -0,0 +1,18 @@
+#include "Field.h"
+
+Field::Field( QString aName, QString aStyle ):
+ m_name( aName )
+ {
+ if( aStyle.isEmpty() )
+ m_style = FieldStyleFactory::DefaultStyle;
+ else
+ m_style = aStyle;
+ }
+
+void Field::setName( const QString aName )
+ {
+ // Save the first original name
+ if( m_oldName.isEmpty() )
+ m_oldName = m_name;
+ m_name = aName;
+ }
diff --git a/src/dictionary/Field.h b/src/dictionary/Field.h
new file mode 100644
index 0000000..08b0123
--- /dev/null
+++ b/src/dictionary/Field.h
@@ -0,0 +1,46 @@
+#ifndef FIELD_H
+#define FIELD_H
+
+#include "../field-styles/FieldStyle.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QString>
+
+/**
+ Field is a field of the dictionary entries. The fields are owned by the dictionary.
+ Dictionary entries and card packs reuse the fields of the dictionary.
+
+ Field id is its name. It's unique.
+
+ When the field is renamed, its first original name is always saved in m_oldName.
+ */
+
+class Field
+{
+public:
+ Field() {}
+ Field( QString aName, QString aStyle = "" );
+
+public:
+ QString name() const {return m_name;}
+ QString oldName() const {return m_oldName;}
+ QString style() const {return m_style;}
+ bool operator<( const Field& anotherField ) const { return m_name < anotherField.name(); }
+ bool operator==( const Field& anotherField ) const { return m_name == anotherField.name(); }
+ QString id04() const { return m_id04; }
+
+ void setName( const QString aName );
+ void setStyle( QString aStyle ) { m_style = aStyle; }
+ void setId04( const QString& aId ) { m_id04 = aId; }
+
+private:
+ QString m_name;
+ QString m_style;
+ QString m_oldName; // For renaming
+
+ // Obsolete:
+ QString m_id04; ///< ID, compatible with v. 0.4
+
+};
+
+#endif
diff --git a/src/dictionary/ICardPack.cpp b/src/dictionary/ICardPack.cpp
new file mode 100644
index 0000000..d2ef03e
--- /dev/null
+++ b/src/dictionary/ICardPack.cpp
@@ -0,0 +1,23 @@
+#include "ICardPack.h"
+#include "Field.h"
+
+const QString ICardPack::SynonymDelimiter = ";";
+const QString ICardPack::HomonymDelimiter = "; ";
+
+QString ICardPack::getQuestionFieldName() const
+{
+ const Field* questionField = getQuestionField();
+ if(questionField)
+ return questionField->name();
+ else
+ return QString();
+}
+
+QStringList ICardPack::getAnswerFieldNames() const
+{
+ QStringList list;
+ foreach(const Field* field, getAnswerFields())
+ if(field)
+ list << field->name();
+ return list;
+}
diff --git a/src/dictionary/ICardPack.h b/src/dictionary/ICardPack.h
new file mode 100644
index 0000000..9c8e4a2
--- /dev/null
+++ b/src/dictionary/ICardPack.h
@@ -0,0 +1,33 @@
+#ifndef ICARDPACK_H
+#define ICARDPACK_H
+
+#include <QString>
+#include <QList>
+
+#include "../study/StudyRecord.h"
+
+class DicRecord;
+class Field;
+
+class ICardPack
+{
+public:
+ static const QString SynonymDelimiter;
+ static const QString HomonymDelimiter;
+
+public:
+ virtual ~ICardPack() {}
+
+ virtual void addStudyRecord(const QString aCardId,
+ const StudyRecord& aStudyRecord) = 0;
+ virtual QList<StudyRecord> getStudyRecords(QString cardId) const = 0;
+ virtual StudyRecord getStudyRecord(QString cardId) const = 0;
+
+ virtual QList<DicRecord*> getRecords() const = 0;
+ virtual const Field* getQuestionField() const = 0;
+ virtual QList<const Field*> getAnswerFields() const = 0;
+ QString getQuestionFieldName() const;
+ QStringList getAnswerFieldNames() const;
+};
+
+#endif
diff --git a/src/dictionary/IDictionary.cpp b/src/dictionary/IDictionary.cpp
new file mode 100644
index 0000000..20bac26
--- /dev/null
+++ b/src/dictionary/IDictionary.cpp
@@ -0,0 +1,59 @@
+#include "IDictionary.h"
+#include "DicRecord.h"
+#include "CardPack.h"
+
+IDictionary::~IDictionary()
+{
+ foreach(DicRecord* record, records)
+ delete record;
+}
+
+void IDictionary::addRecord(DicRecord* record)
+{
+ records << record;
+ notifyRecordsInserted(records.size() - 1, 1);
+}
+
+void IDictionary::addRecords(const QList<DicRecord*>& records)
+{
+ this->records << records;
+ notifyRecordsInserted(records.size() - records.size(), records.size());
+}
+
+QString IDictionary::extendImagePaths(QString text) const
+{
+ text = replaceImagePaths(text, "%", QFileInfo(filePath).path());
+ text = replaceImagePaths(text, "%%", getImagesPath());
+ return text;
+}
+
+QString IDictionary::getImagesPath() const
+{
+ QString path = QFileInfo(filePath).path();
+ QString baseName = QFileInfo(filePath).completeBaseName();
+ return QDir(path).filePath(baseName);
+}
+
+QString IDictionary::replaceImagePaths(QString text, const QString& shortDir,
+ const QString& replacingPath)
+{
+ QRegExp imgRx(QString("(<img src=\")%1([/\\\\])").arg(shortDir));
+ text.replace(imgRx, QString("\\1%1\\2").arg(replacingPath));
+ return text;
+}
+
+int IDictionary::countTodaysAllCards() const
+ {
+ int num = 0;
+ foreach(CardPack* pack, m_cardPacks)
+ num += pack->getTodayReviewedCardsNum();
+ return num;
+ }
+
+int IDictionary::countTodaysNewCards() const
+ {
+ int num = 0;
+ foreach(CardPack* pack, m_cardPacks)
+ num += pack->getTodayNewCardsNum();
+ return num;
+ }
diff --git a/src/dictionary/IDictionary.h b/src/dictionary/IDictionary.h
new file mode 100644
index 0000000..bfc82bc
--- /dev/null
+++ b/src/dictionary/IDictionary.h
@@ -0,0 +1,51 @@
+#ifndef IDICTIONARY_H
+#define IDICTIONARY_H
+
+#include <QtCore>
+
+class DicRecord;
+class Field;
+class CardPack;
+
+class IDictionary
+{
+public:
+ static const int AllFields = -1;
+
+public:
+ IDictionary(const QString& filePath = ""):
+ filePath(filePath) {}
+ virtual ~IDictionary();
+
+ void addRecord(DicRecord* record);
+ void addRecords(const QList<DicRecord*>& records);
+ QList<DicRecord*> getRecords() const { return records; }
+
+ virtual const Field* field( int aIx ) const = 0;
+ virtual const Field* field( const QString aFieldName ) const = 0;
+ virtual int indexOfCardPack( CardPack* aPack ) const = 0;
+
+ virtual void addCardPack(CardPack* aCardPack) = 0;
+
+ virtual QFile::FileError saveStudy() { return QFile::NoError; }
+
+ // Stats
+ int countTodaysAllCards() const;
+ int countTodaysNewCards() const;
+
+ QString extendImagePaths(QString text) const;
+ QString getImagesPath() const;
+
+protected:
+ virtual void notifyRecordsInserted(int /*index*/, int /*num*/) {}
+
+private:
+ static QString replaceImagePaths(QString text, const QString& shortDir,
+ const QString& replacingPath);
+
+protected:
+ QString filePath;
+ QList<DicRecord*> records;
+ QList<CardPack*> m_cardPacks;
+};
+#endif
diff --git a/src/dictionary/TreeItem.h b/src/dictionary/TreeItem.h
new file mode 100644
index 0000000..7c65cd0
--- /dev/null
+++ b/src/dictionary/TreeItem.h
@@ -0,0 +1,25 @@
+#ifndef TREEITEM_H
+#define TREEITEM_H
+
+#include <QVariant>
+
+class TreeItem: public QObject
+{
+public:
+ TreeItem() {}
+ virtual ~TreeItem() {}
+
+ virtual const TreeItem* parent() const = 0;
+ virtual const TreeItem* child( int aRow ) const = 0;
+ virtual int childCount() const = 0;
+ virtual int columnCount() const = 0;
+ virtual QVariant data( int aColumn ) const = 0;
+ virtual int row() const = 0;
+ virtual int topParentRow() const = 0;
+
+protected:
+ virtual void entryChanged(int, int) {};
+ virtual void entriesRemoved(int, int) {};
+};
+
+#endif // TREEITEM_H
diff --git a/src/export-import/CsvData.h b/src/export-import/CsvData.h
new file mode 100644
index 0000000..c6ea9ad
--- /dev/null
+++ b/src/export-import/CsvData.h
@@ -0,0 +1,36 @@
+#ifndef CSVDATA_H
+#define CSVDATA_H
+
+#include <QtCore>
+
+enum FieldSeparationMode
+{
+ EFieldSeparatorAnyCharacter = 0,
+ EFieldSeparatorAnyCombination,
+ EFieldSeparatorExactString
+};
+
+struct CsvParams
+{
+ QString fieldSeparators;
+ QChar textDelimiter;
+ QChar commentChar;
+};
+
+struct CsvImportData: public CsvParams
+{
+ QTextCodec* textCodec;
+ int fromLine;
+ bool firstLineIsHeader;
+ FieldSeparationMode fieldSeparationMode;
+ int colsToImport; // 0 = All columns
+};
+
+struct CsvExportData: public CsvParams
+{
+ QList<int> usedCols; // Empty = use all columns
+ bool quoteAllFields;
+ bool writeColumnNames;
+};
+
+#endif
diff --git a/src/export-import/CsvDialog.cpp b/src/export-import/CsvDialog.cpp
new file mode 100644
index 0000000..b19d029
--- /dev/null
+++ b/src/export-import/CsvDialog.cpp
@@ -0,0 +1,176 @@
+#include "CsvDialog.h"
+
+QChar CsvDialog::SpaceChar(0x2423);
+QChar CsvDialog::TabChar(0x21A6);
+QString CsvDialog::ExtendedTab(QString(QChar(0x21A6)) + "\t");
+
+CsvDialog::CsvDialog(QWidget* parent):
+ QDialog(parent)
+{
+ resize(600, 550);
+}
+
+void CsvDialog::init()
+{
+ setLayout(createMainLayout());
+ updatePreview();
+ connectControlsToPreview();
+}
+
+void CsvDialog::connectControlsToPreview()
+{
+ connect(separatorsEdit, SIGNAL(textChanged(QString)), SLOT(updatePreview()) );
+ connect(textDelimiterCB, SIGNAL(stateChanged(int)), SLOT(updatePreview()) );
+ connect(textDelimiterCombo, SIGNAL(editTextChanged(QString)), SLOT(updatePreview()) );
+ connect(commentCharCombo, SIGNAL(editTextChanged(QString)), SLOT(updatePreview()) );
+}
+
+QLayout* CsvDialog::createMainLayout()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addLayout(createTopLayout());
+ lt->addWidget(new QLabel(tr("Preview:")));
+ lt->addLayout(createPreviewLt());
+ lt->addWidget(createButtonBox());
+ return lt;
+}
+
+QLayout* CsvDialog::createTopLayout()
+{
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(createLeftGroup());
+ lt->addWidget(createSeparatorsGroup());
+ return lt;
+}
+
+QWidget* CsvDialog::createCharSetCombo()
+{
+ charSetCombo = new QComboBox;
+ charSetCombo->setEditable(false);
+ QList<QByteArray> codecNames = QTextCodec::availableCodecs();
+ qSort( codecNames );
+ for( int i=0; i<codecNames.size(); i++)
+ charSetCombo->addItem(codecNames[i],
+ QTextCodec::codecForName(codecNames[i] )->mibEnum());
+ int utf8Ix = codecNames.indexOf( "UTF-8" );
+ charSetCombo->setCurrentIndex( utf8Ix );
+ return charSetCombo;
+}
+
+QGroupBox* CsvDialog::createLeftGroup()
+{
+ QGroupBox* group = new QGroupBox(getLeftGroupTitle());
+ group->setLayout(createLeftGroupLayout());
+ return group;
+}
+
+QGroupBox* CsvDialog::createSeparatorsGroup()
+{
+ QGroupBox* group = new QGroupBox(tr("Separators"));
+ group->setLayout(createSeparatorsLayout());
+ return group;
+}
+
+QDialogButtonBox* CsvDialog::createButtonBox()
+{
+ QDialogButtonBox* buttonBox = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ connect(buttonBox, SIGNAL(accepted()), SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ return buttonBox;
+}
+
+QWidget* CsvDialog::createFieldSeparatorWidget(const QString& additionalSeparators)
+{
+ separatorsEdit = new QLineEdit(QString(TabChar) + "&" + additionalSeparators);
+ connect(separatorsEdit, SIGNAL(textChanged(const QString&)),
+ SLOT(showInvisibleInSeparatorsEdit()));
+
+ QPushButton* addSeparatorBtn = new QPushButton(tr("Ta&b"));
+ addSeparatorBtn->setMinimumWidth(40);
+ connect(addSeparatorBtn, SIGNAL(clicked()), SLOT(insertTabToSeparators()));
+
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(separatorsEdit);
+ lt->addWidget(addSeparatorBtn);
+ lt->setContentsMargins(QMargins());
+
+ // Workaround for a bug in QFormLayout::addRow(text, layout)
+ QWidget* widget = new QWidget;
+ widget->setLayout(lt);
+ return widget;
+}
+
+QWidget* CsvDialog::createTextDelimiterBox()
+{
+ textDelimiterCB = new QCheckBox(tr("&Text delimiter:"));
+ textDelimiterCB->setChecked(true);
+ return textDelimiterCB;
+}
+
+QWidget* CsvDialog::createTextDelimiterCombo()
+{
+ return textDelimiterCombo = createEditableCombo(QStringList() << "\"" << "\'");
+}
+
+QWidget* CsvDialog::createCommentCharCombo()
+{
+ return commentCharCombo = createEditableCombo(QStringList() << "#" << ";");
+}
+
+QComboBox* CsvDialog::createEditableCombo(const QStringList& items)
+{
+ QComboBox* combo = new QComboBox;
+ combo->setEditable(true);
+ combo->setInsertPolicy(QComboBox::NoInsert);
+ combo->setValidator(new QRegExpValidator(QRegExp("\\S{1}"), this));
+ combo->addItems(items);
+ return combo;
+}
+
+QChar CsvDialog::getTextDelimiterChar() const
+{
+ if(textDelimiterCB->isChecked() && !textDelimiterCombo->currentText().isEmpty())
+ return textDelimiterCombo->currentText()[0];
+ else
+ return QChar(0);
+}
+
+void CsvDialog::showInvisibleInSeparatorsEdit()
+{
+ separatorsEdit->setText(setCharVisibility(separatorsEdit->text(), true));
+}
+
+void CsvDialog::insertTabToSeparators()
+{
+ separatorsEdit->insert(TabChar);
+}
+
+QTextCodec* CsvDialog::getTextCodec()
+{
+ return QTextCodec::codecForMib(
+ charSetCombo->itemData(charSetCombo->currentIndex()).toInt());
+}
+
+/// Convert invisible chars like space and tab to visible equivalents or vice versa.
+QString CsvDialog::setCharVisibility(const QString& input, bool visible)
+{
+ QString res = input;
+ if(visible)
+ {
+ res.replace(' ', SpaceChar);
+ res.replace('\t', ExtendedTab);
+ }
+ else
+ {
+ res.replace(SpaceChar, ' ');
+ res.replace(ExtendedTab, "\t");
+ res.replace(TabChar, '\t');
+ }
+ return res;
+}
+
+void CsvDialog::updateTextDelimiterCombo()
+{
+ textDelimiterCombo->setEnabled( textDelimiterCB->isChecked() );
+}
diff --git a/src/export-import/CsvDialog.h b/src/export-import/CsvDialog.h
new file mode 100644
index 0000000..f50cd62
--- /dev/null
+++ b/src/export-import/CsvDialog.h
@@ -0,0 +1,62 @@
+#ifndef CSV_DIALOG_H
+#define CSV_DIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class CsvDialog: public QDialog
+{
+ Q_OBJECT
+
+public:
+ CsvDialog(QWidget* parent);
+
+protected:
+ static QString setCharVisibility(const QString& input, bool visible);
+
+protected:
+ void init();
+ virtual QLayout* createLeftGroupLayout() = 0;
+ virtual QString getLeftGroupTitle() = 0;
+ QWidget* createCharSetCombo();
+ virtual QLayout* createSeparatorsLayout() = 0;
+ QWidget* createFieldSeparatorWidget(const QString& additionalSeparators = "");
+ QWidget* createTextDelimiterBox();
+ QWidget* createTextDelimiterCombo();
+ QWidget* createCommentCharCombo();
+ QComboBox* createEditableCombo(const QStringList& items);
+ virtual QLayout* createPreviewLt() = 0;
+ QDialogButtonBox* createButtonBox();
+ QTextCodec* getTextCodec();
+ QChar getTextDelimiterChar() const;
+
+protected slots:
+ virtual void updatePreview() = 0;
+ void updateTextDelimiterCombo();
+
+private:
+ QLayout* createMainLayout();
+ void connectControlsToPreview();
+ QLayout* createTopLayout();
+ QGroupBox* createLeftGroup();
+ QGroupBox* createSeparatorsGroup();
+ void doUpdatePreview() { updatePreview(); }
+
+private slots:
+ void showInvisibleInSeparatorsEdit();
+ void insertTabToSeparators();
+
+protected:
+ static QChar SpaceChar;
+ static QChar TabChar;
+ static QString ExtendedTab;
+
+protected:
+ QComboBox* charSetCombo;
+ QLineEdit* separatorsEdit;
+ QCheckBox* textDelimiterCB;
+ QComboBox* textDelimiterCombo;
+ QComboBox* commentCharCombo;
+};
+
+#endif
diff --git a/src/export-import/CsvExportDialog.cpp b/src/export-import/CsvExportDialog.cpp
new file mode 100644
index 0000000..9e4dc90
--- /dev/null
+++ b/src/export-import/CsvExportDialog.cpp
@@ -0,0 +1,137 @@
+#include "CsvExportDialog.h"
+
+#include "../strings.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicCsvWriter.h"
+
+CsvExportDialog::CsvExportDialog(QWidget* parent, const Dictionary* dict):
+ CsvDialog(parent),
+ dictionary(dict),
+ dicWriter(new DicCsvWriter(dict))
+{
+ init();
+ setWindowTitle(tr("Export to CSV"));
+
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(updateTextDelimiterCombo()) );
+ connect( showInvisibleCharsCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateCharVisibility()) );
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateQuoteAllFieldsCB()) );
+
+ connect( usedColsEdit, SIGNAL(textChanged(QString)), this, SLOT(updatePreview()) );
+ connect( quoteAllFieldsCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( writeColumnNamesCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+}
+
+CsvExportDialog::~CsvExportDialog()
+{
+ delete dicWriter;
+}
+
+QLayout* CsvExportDialog::createLeftGroupLayout()
+{
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("C&haracter set:"), createCharSetCombo());
+ lt->addRow(tr("Used &columns:"), createUsedColsEdit());
+ lt->addRow(createWriteColumnNamesBox());
+ return lt;
+}
+
+QWidget* CsvExportDialog::createUsedColsEdit()
+{
+ usedColsEdit = new QLineEdit("123");
+ usedColsEdit->setValidator(new QRegExpValidator(QRegExp("[1-9]+"), this));
+ return usedColsEdit;
+}
+
+QWidget* CsvExportDialog::createWriteColumnNamesBox()
+{
+ writeColumnNamesCB = new QCheckBox(tr("Write column &names"));
+ writeColumnNamesCB->setChecked( true );
+ return writeColumnNamesCB;
+}
+
+QLayout* CsvExportDialog::createSeparatorsLayout()
+{
+ quoteAllFieldsCB = new QCheckBox(tr("&Quote all fields"));
+
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("Field &separator:"), createFieldSeparatorWidget(SpaceChar));
+ lt->addRow(createTextDelimiterBox(), createTextDelimiterCombo());
+ lt->addRow(quoteAllFieldsCB);
+ lt->addRow(tr("Co&mment character:"), createCommentCharCombo());
+ return lt;
+}
+
+QLayout* CsvExportDialog::createPreviewLt()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addWidget(createPreview());
+ lt->addWidget(createShowInvisibleBox());
+ return lt;
+}
+
+QWidget* CsvExportDialog::createPreview()
+{
+ csvPreview = new QTextEdit;
+ csvPreview->setReadOnly(true);
+ csvPreview->setLineWrapMode( QTextEdit::NoWrap );
+ csvPreview->setFontPointSize( 10 );
+ return csvPreview;
+}
+
+QCheckBox* CsvExportDialog::createShowInvisibleBox()
+{
+ showInvisibleCharsCB = new QCheckBox(tr("Show &invisible characters"));
+ showInvisibleCharsCB->setChecked( false );
+ return showInvisibleCharsCB;
+}
+
+void CsvExportDialog::UpdateQuoteAllFieldsCB()
+{
+ quoteAllFieldsCB->setEnabled( textDelimiterCB->isChecked() );
+}
+
+void CsvExportDialog::updatePreview()
+{
+ CsvExportData params;
+ params.usedCols = getUsedColumns();
+ params.fieldSeparators = setCharVisibility( separatorsEdit->text(), false );
+ params.textDelimiter = getTextDelimiterChar();
+ params.quoteAllFields = quoteAllFieldsCB->isChecked();
+ params.commentChar = commentCharCombo->currentText()[0];
+ params.writeColumnNames = writeColumnNamesCB->isChecked();
+
+ QString csvString = dicWriter->toCsvString( params );
+ csvPreview->setPlainText(
+ setCharVisibility(csvString, showInvisibleCharsCB->isChecked()));
+}
+
+QList<int> CsvExportDialog::getUsedColumns()
+{
+ QList<int> res;
+ for(int i = 0; i < usedColsEdit->text().length(); i++)
+ {
+ int col = usedColsEdit->text()[i].digitValue();
+ if(col > 0 && col <= dictionary->fieldsNum())
+ res << col - 1;
+ }
+ return res;
+}
+
+void CsvExportDialog::SaveCSVToFile( const QString& aFilePath )
+{
+ QFile file( aFilePath );
+ if( !file.open( QIODevice::WriteOnly | QFile::Text ) ) // \r\n --> \n
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), tr("Cannot save to file:\n %1.").arg(aFilePath) );
+ return;
+ }
+ QTextStream outStream( &file );
+ outStream.setCodec( getTextCodec() );
+ outStream << setCharVisibility( csvPreview->toPlainText(), false );
+}
+
+void CsvExportDialog::UpdateCharVisibility()
+{
+ csvPreview->setPlainText(
+ setCharVisibility(csvPreview->toPlainText(), showInvisibleCharsCB->isChecked()) );
+}
diff --git a/src/export-import/CsvExportDialog.h b/src/export-import/CsvExportDialog.h
new file mode 100644
index 0000000..d99c421
--- /dev/null
+++ b/src/export-import/CsvExportDialog.h
@@ -0,0 +1,52 @@
+#ifndef CSVEXPORTDIALOG_H
+#define CSVEXPORTDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "CsvDialog.h"
+
+class Dictionary;
+class DicCsvWriter;
+
+class CsvExportDialog: public CsvDialog
+{
+ Q_OBJECT
+
+public:
+ CsvExportDialog(QWidget* parent, const Dictionary* dict );
+ ~CsvExportDialog();
+
+ void SaveCSVToFile( const QString& aFilePath );
+
+protected:
+ QLayout* createLeftGroupLayout();
+ QString getLeftGroupTitle() { return tr("Output"); }
+ QLayout* createSeparatorsLayout();
+ QLayout* createPreviewLt();
+
+protected slots:
+ void updatePreview();
+
+private slots:
+ void UpdateQuoteAllFieldsCB();
+ void UpdateCharVisibility();
+
+private:
+ QWidget* createUsedColsEdit();
+ QWidget* createWriteColumnNamesBox();
+ QList<int> getUsedColumns();
+ QWidget* createPreview();
+ QCheckBox* createShowInvisibleBox();
+
+private:
+ const Dictionary* dictionary; // not own, created here
+ DicCsvWriter* dicWriter;
+ QLineEdit* usedColsEdit;
+ QCheckBox* writeColumnNamesCB;
+ QCheckBox* quoteAllFieldsCB;
+ QTextEdit* csvPreview;
+ QCheckBox* showInvisibleCharsCB;
+};
+
+#endif
diff --git a/src/export-import/CsvImportDialog.cpp b/src/export-import/CsvImportDialog.cpp
new file mode 100644
index 0000000..51c3c87
--- /dev/null
+++ b/src/export-import/CsvImportDialog.cpp
@@ -0,0 +1,155 @@
+#include "CsvImportDialog.h"
+
+#include "../main-view/DictTableView.h"
+#include "../main-view/DictTableModel.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicCsvReader.h"
+
+CsvImportDialog::CsvImportDialog(QWidget* parent, QString filePath, const AppModel* appModel):
+ CsvDialog(parent),
+ filePath(filePath),
+ dictionary(NULL),
+ appModel(appModel)
+{
+ init();
+ setWindowTitle(tr("Import from CSV"));
+
+ connect( textDelimiterCB, SIGNAL(stateChanged(int)), this, SLOT(updateTextDelimiterCombo()) );
+ connect( commentCharCB, SIGNAL(stateChanged(int)), this, SLOT(UpdateCommentCharacterCombo()) );
+ connect( this, SIGNAL(rejected()), this, SLOT(DeleteDictionary()) );
+
+ connect( charSetCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updatePreview()) );
+ connect( fromLineSpin, SIGNAL(valueChanged(int)), this, SLOT(updatePreview()) );
+ connect( firstLineIsHeaderCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( anyCharacterRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( anyCombinationRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( exactStringRB, SIGNAL(toggled(bool)), this, SLOT(updatePreview()) );
+ connect( commentCharCB, SIGNAL(stateChanged(int)), this, SLOT(updatePreview()) );
+ connect( colsToImportSpin, SIGNAL(valueChanged(int)), this, SLOT(updatePreview()) );
+}
+
+CsvImportDialog::~CsvImportDialog()
+{
+ delete iPreviewModel;
+ delete dicReader;
+}
+
+QLayout* CsvImportDialog::createLeftGroupLayout()
+{
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("C&haracter set:"), createCharSetCombo());
+ lt->addRow(tr("From &line:"), createFromLineSpin());
+ lt->addRow(tr("Number of colum&ns:"), createColsToImportSpin());
+ lt->addRow(createFirstLineIsHeaderCB());
+ return lt;
+}
+
+QWidget* CsvImportDialog::createFromLineSpin()
+{
+ fromLineSpin = new QSpinBox();
+ fromLineSpin->setRange(1, 100);
+ return fromLineSpin;
+}
+
+QWidget* CsvImportDialog::createColsToImportSpin()
+{
+ colsToImportSpin = new QSpinBox();
+ colsToImportSpin->setRange(0, 9);
+ colsToImportSpin->setSpecialValueText(tr("All"));
+ return colsToImportSpin;
+}
+
+QWidget* CsvImportDialog::createFirstLineIsHeaderCB()
+{
+ firstLineIsHeaderCB = new QCheckBox(tr("&First line has field names"));
+ firstLineIsHeaderCB->setChecked(false);
+ return firstLineIsHeaderCB;
+}
+
+QLayout* CsvImportDialog::createSeparatorsLayout()
+{
+ createSeparationRadioButtons();
+
+ QFormLayout* lt = new QFormLayout;
+ lt->addRow(tr("Field &separator:"),
+ createFieldSeparatorWidget());
+ lt->addRow(tr("Separation mode:"), anyCharacterRB);
+ // The label is a workaround to make enough space for the radio button text
+ lt->addRow(" ", anyCombinationRB);
+ lt->addRow(NULL, exactStringRB);
+ lt->addRow(createTextDelimiterBox(), createTextDelimiterCombo());
+ lt->addRow(createCommentCharBox(), createCommentCharCombo());
+ return lt;
+}
+
+void CsvImportDialog::createSeparationRadioButtons()
+{
+ anyCharacterRB = new QRadioButton(tr("An&y character"));
+ anyCharacterRB->setToolTip(tr("Fields are separated by any separator character"));
+ anyCombinationRB = new QRadioButton(tr("A co&mbination of characters"));
+ anyCombinationRB->setToolTip(tr("Fields are separated by a combination of separator characters, in any order"));
+ anyCombinationRB->setChecked( true );
+ exactStringRB = new QRadioButton(tr("E&xact string"));
+ exactStringRB->setToolTip(tr("Fields are separated by the exact string of separators, in the above defined order"));
+}
+
+QWidget* CsvImportDialog::createCommentCharBox()
+{
+ commentCharCB = new QCheckBox(tr("&Comment character:"));
+ commentCharCB->setChecked(true);
+ return commentCharCB;
+}
+
+QLayout* CsvImportDialog::createPreviewLt()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->addWidget(createPreview());
+ return lt;
+}
+
+QWidget* CsvImportDialog::createPreview()
+{
+ dictionary = new Dictionary( filePath + Dictionary::DictFileExtension, true, appModel );
+ dicReader = new DicCsvReader( dictionary );
+ iPreviewModel = new DictTableModel( dictionary );
+ previewTable = new DictTableView(iPreviewModel);
+ return previewTable;
+}
+
+void CsvImportDialog::UpdateCommentCharacterCombo()
+{
+ commentCharCombo->setEnabled( commentCharCB->isChecked() );
+}
+
+void CsvImportDialog::updatePreview()
+{
+ CsvImportData params;
+ params.textCodec = getTextCodec();
+ params.fromLine = fromLineSpin->value();
+ params.fieldSeparators = setCharVisibility(separatorsEdit->text(), false);
+ params.fieldSeparationMode = getSeparationMode();
+ params.commentChar = commentCharCB->isChecked()?
+ commentCharCombo->currentText()[0]: QChar(0);
+ params.textDelimiter = getTextDelimiterChar();
+ params.colsToImport = colsToImportSpin->value();
+ params.firstLineIsHeader = firstLineIsHeaderCB->isChecked();
+
+ dicReader->readDict( filePath, params );
+ iPreviewModel->resetData();
+}
+
+FieldSeparationMode CsvImportDialog::getSeparationMode()
+{
+ if(anyCharacterRB->isChecked())
+ return EFieldSeparatorAnyCharacter;
+ else if (anyCombinationRB->isChecked())
+ return EFieldSeparatorAnyCombination;
+ else
+ return EFieldSeparatorExactString;
+}
+
+void CsvImportDialog::DeleteDictionary()
+{
+delete dictionary;
+dictionary = NULL;
+}
diff --git a/src/export-import/CsvImportDialog.h b/src/export-import/CsvImportDialog.h
new file mode 100644
index 0000000..bc3a9d1
--- /dev/null
+++ b/src/export-import/CsvImportDialog.h
@@ -0,0 +1,64 @@
+#ifndef CSVIMPORTDIALOG_H
+#define CSVIMPORTDIALOG_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "CsvData.h"
+#include "CsvDialog.h"
+
+class DictTableView;
+class Dictionary;
+class DicCsvReader;
+class DictTableModel;
+class AppModel;
+
+class CsvImportDialog: public CsvDialog
+{
+ Q_OBJECT
+
+public:
+ CsvImportDialog(QWidget* parent, QString filePath, const AppModel* appModel );
+ ~CsvImportDialog();
+
+ Dictionary* getDictionary() { return dictionary; }
+
+protected:
+ QLayout* createLeftGroupLayout();
+ QString getLeftGroupTitle() { return tr("Input"); }
+ QLayout* createSeparatorsLayout();
+ QLayout* createPreviewLt();
+
+protected slots:
+ void updatePreview();
+
+private slots:
+ void UpdateCommentCharacterCombo();
+ void DeleteDictionary();
+
+private:
+ QWidget* createFromLineSpin();
+ QWidget* createColsToImportSpin();
+ QWidget* createFirstLineIsHeaderCB();
+ void createSeparationRadioButtons();
+ FieldSeparationMode getSeparationMode();
+ QWidget* createCommentCharBox();
+ QWidget* createPreview();
+
+private:
+ QString filePath;
+ Dictionary* dictionary; // not own, created here
+ const AppModel* appModel;
+ DicCsvReader* dicReader;
+ DictTableModel* iPreviewModel;
+ QSpinBox* fromLineSpin; // 1-based
+ QSpinBox* colsToImportSpin; // 1-based; 0 = all
+ QCheckBox* firstLineIsHeaderCB;
+ QRadioButton* anyCharacterRB;
+ QRadioButton* anyCombinationRB;
+ QRadioButton* exactStringRB;
+ QCheckBox* commentCharCB;
+ DictTableView* previewTable;
+};
+
+#endif
diff --git a/src/field-styles/FieldStyle.cpp b/src/field-styles/FieldStyle.cpp
new file mode 100644
index 0000000..4e710f8
--- /dev/null
+++ b/src/field-styles/FieldStyle.cpp
@@ -0,0 +1,38 @@
+#include "FieldStyle.h"
+
+FieldStyle::FieldStyle():
+ FieldStyle("", 0) {}
+
+FieldStyle::FieldStyle(const QString& family, int size,
+ bool bold, bool italic, const QString& colorName,
+ const QString& prefix, const QString& suffix,
+ bool hasKeyword, const QString& keywordColorName):
+ color(Qt::black),
+ prefix(prefix), suffix(suffix),
+ hasKeyword(hasKeyword),
+ keywordBold(false),
+ keywordItalic(false)
+{
+ font.setFamily("Times New Roman");
+ font.setPointSize(18);
+ if(!family.isEmpty())
+ font.setFamily(family);
+ if(size > 0)
+ font.setPointSize(size);
+ font.setBold(bold);
+ font.setItalic(italic);
+ if(!colorName.isEmpty())
+ color.setNamedColor(colorName);
+ keywordColor.setNamedColor(keywordColorName);
+}
+
+FieldStyle FieldStyle::getKeywordStyle() const
+{
+ if(!hasKeyword)
+ return *this;
+ FieldStyle keywordStyle = *this;
+ keywordStyle.font.setBold(keywordBold);
+ keywordStyle.font.setItalic(keywordItalic);
+ keywordStyle.color = keywordColor;
+ return keywordStyle;
+}
diff --git a/src/field-styles/FieldStyle.h b/src/field-styles/FieldStyle.h
new file mode 100644
index 0000000..20da858
--- /dev/null
+++ b/src/field-styles/FieldStyle.h
@@ -0,0 +1,28 @@
+#ifndef FIELDSTYLE_H
+#define FIELDSTYLE_H
+
+#include <QFont>
+#include <QColor>
+
+class FieldStyle
+{
+public:
+ FieldStyle();
+ FieldStyle(const QString& family, int size,
+ bool bold = true, bool italic = false, const QString& colorName = "",
+ const QString& prefix = "", const QString& suffix = "",
+ bool hasKeyword = false, const QString& keywordColorName = "#000000");
+ FieldStyle getKeywordStyle() const;
+
+public:
+ QFont font;
+ QColor color;
+ QString prefix;
+ QString suffix;
+ bool hasKeyword;
+ bool keywordBold;
+ bool keywordItalic;
+ QColor keywordColor;
+};
+
+#endif
diff --git a/src/field-styles/FieldStyleFactory.cpp b/src/field-styles/FieldStyleFactory.cpp
new file mode 100644
index 0000000..ab789e3
--- /dev/null
+++ b/src/field-styles/FieldStyleFactory.cpp
@@ -0,0 +1,145 @@
+#include "FieldStyleFactory.h"
+
+const QString FieldStyleFactory::DefaultStyle = "Normal";
+
+FieldStyleFactory::FieldStyleFactory()
+ {
+ cardBgColor.setNamedColor("white");
+ initStyles();
+ }
+
+void FieldStyleFactory::initStyles()
+{
+ setStyle(DefaultStyle, FieldStyle());
+ setStyle("Example", {"", 14, false, false, "", "", "", true, "blue"});
+ setStyle("Transcription", {"Arial", 0, false, false, "", "/", "/"});
+ setStyle("Big", {"Arial", 26});
+ setStyle("Color1", {"", 0, true, false, "red"});
+ setStyle("Color2", {"", 0, true, false, "blue"});
+}
+
+FieldStyleFactory* FieldStyleFactory::inst()
+ {
+ static FieldStyleFactory factory;
+ return &factory;
+ }
+
+void FieldStyleFactory::setStyle(const QString& aStyleName, FieldStyle aStyle )
+ {
+ styles[aStyleName] = aStyle;
+ if(!stylesNames.contains( aStyleName ))
+ stylesNames << aStyleName;
+ }
+
+void FieldStyleFactory::load()
+{
+ QSettings settings;
+ settings.beginGroup("Styles");
+ cardBgColor.setNamedColor(
+ settings.value("bg-color", cardBgColor.name()).toString());
+ foreach(QString styleName,
+ settings.value("list", getStyleNames()).toStringList())
+ loadStyle(settings, styleName);
+ settings.endGroup();
+}
+
+void FieldStyleFactory::loadStyle(QSettings& settings, const QString& styleName)
+{
+ FieldStyle curStyle;
+ settings.beginGroup(styleName);
+ FieldStyle defaultStyle = getStyle(styleName);
+ loadStyleMainProperties(settings, curStyle, defaultStyle);
+ loadKeywordStyle(settings, curStyle, defaultStyle);
+ settings.endGroup();
+ setStyle(styleName, curStyle);
+}
+
+void FieldStyleFactory::loadStyleMainProperties(const QSettings& settings,
+ FieldStyle& curStyle, const FieldStyle& defaultStyle)
+{
+ curStyle.font.setFamily(
+ settings.value("font-family", defaultStyle.font.family()).toString());
+ curStyle.font.setPointSize(
+ settings.value("font-size", defaultStyle.font.pointSize()).toInt());
+ curStyle.font.setBold(
+ settings.value("font-bold", defaultStyle.font.bold()).toBool());
+ curStyle.font.setItalic(
+ settings.value("font-italic", defaultStyle.font.italic()).toBool());
+ curStyle.color.setNamedColor(
+ settings.value("color", defaultStyle.color.name()).toString());
+ curStyle.prefix = settings.value("prefix", defaultStyle.prefix).toString();
+ curStyle.suffix = settings.value("suffix", defaultStyle.suffix).toString();
+ curStyle.hasKeyword = settings.value("keyword", defaultStyle.hasKeyword).toBool();
+}
+
+void FieldStyleFactory::loadKeywordStyle(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(!curStyle.hasKeyword)
+ return;
+ curStyle.keywordBold =
+ settings.value("keyword/font-bold", defaultStyle.keywordBold).toBool();
+ curStyle.keywordItalic =
+ settings.value("keyword/font-italic", defaultStyle.keywordItalic).toBool();
+ curStyle.keywordColor.setNamedColor(
+ settings.value("keyword/color", defaultStyle.keywordColor.name()).toString());
+}
+
+void FieldStyleFactory::save()
+ {
+ QSettings settings;
+ settings.beginGroup("Styles");
+ settings.remove(""); // Remove old user settings
+ if(cardBgColor != FieldStyleFactory().cardBgColor)
+ settings.setValue("bg-color", cardBgColor.name());
+ foreach(QString styleName, getStyleNames())
+ saveStyle(settings, styleName);
+ settings.endGroup();
+ }
+
+void FieldStyleFactory::saveStyle(QSettings& settings, const QString& styleName)
+{
+ const FieldStyle curStyle = getStyle(styleName);
+ const FieldStyle defaultStyle = FieldStyleFactory().getStyle(styleName);
+ settings.beginGroup( styleName );
+ saveStyleMainProperties(settings, curStyle, defaultStyle);
+ saveKeywordStyle(settings, curStyle, defaultStyle);
+ settings.endGroup();
+}
+
+void FieldStyleFactory::saveStyleMainProperties(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(curStyle.font.family() != defaultStyle.font.family())
+ settings.setValue("font-family", curStyle.font.family());
+ if(curStyle.font.pointSize() != defaultStyle.font.pointSize())
+ settings.setValue("font-size", curStyle.font.pointSize());
+ if(curStyle.font.bold() != defaultStyle.font.bold())
+ settings.setValue("font-bold", curStyle.font.bold());
+ if(curStyle.font.italic() != defaultStyle.font.italic())
+ settings.setValue("font-italic", curStyle.font.italic());
+ if(curStyle.color != defaultStyle.color)
+ settings.setValue("color", curStyle.color.name());
+ if(curStyle.prefix != defaultStyle.prefix)
+ settings.setValue("prefix", curStyle.prefix);
+ if(curStyle.suffix != defaultStyle.suffix)
+ settings.setValue("suffix", curStyle.suffix);
+ if(curStyle.hasKeyword != defaultStyle.hasKeyword)
+ settings.setValue("keyword", curStyle.hasKeyword);
+}
+
+void FieldStyleFactory::saveKeywordStyle(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle)
+{
+ if(!curStyle.hasKeyword)
+ return;
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordBold != defaultStyle.keywordBold)
+ settings.setValue("keyword/font-bold", curStyle.keywordBold);
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordItalic != defaultStyle.keywordItalic)
+ settings.setValue("keyword/font-italic", curStyle.keywordItalic);
+ if(!defaultStyle.hasKeyword ||
+ curStyle.keywordColor != defaultStyle.keywordColor)
+ settings.setValue("keyword/color", curStyle.keywordColor.name());
+}
diff --git a/src/field-styles/FieldStyleFactory.h b/src/field-styles/FieldStyleFactory.h
new file mode 100644
index 0000000..072bdd5
--- /dev/null
+++ b/src/field-styles/FieldStyleFactory.h
@@ -0,0 +1,49 @@
+#ifndef FIELDSTYLEFACTORY_H
+#define FIELDSTYLEFACTORY_H
+
+#include "FieldStyle.h"
+
+#include <QtCore>
+
+class FieldStyleFactory
+{
+public:
+ static FieldStyleFactory* inst();
+
+public:
+ FieldStyleFactory();
+ const QStringList getStyleNames() const
+ { return stylesNames; }
+ const FieldStyle getStyle(const QString& aStyleName) const
+ { return styles[aStyleName]; }
+ FieldStyle* getStylePtr( const QString& aStyleName )
+ { return &( styles[aStyleName] ); }
+ void setStyle(const QString& aStyleName, FieldStyle aStyle );
+ void load();
+ void save();
+
+private:
+ void initStyles();
+ void loadStyle(QSettings& settings, const QString& styleName);
+ void loadStyleMainProperties(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void loadKeywordStyle(const QSettings& settings, FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void saveStyle(QSettings& settings, const QString& styleName);
+ void saveStyleMainProperties(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+ void saveKeywordStyle(QSettings& settings, const FieldStyle& curStyle,
+ const FieldStyle& defaultStyle);
+
+public:
+ static const QString DefaultStyle;
+
+public:
+ QColor cardBgColor;
+
+private:
+ QHash<QString, FieldStyle> styles;
+ QStringList stylesNames; // Keeps order of styles
+};
+
+#endif
diff --git a/src/main-view/AboutDialog.cpp b/src/main-view/AboutDialog.cpp
new file mode 100644
index 0000000..f1a7839
--- /dev/null
+++ b/src/main-view/AboutDialog.cpp
@@ -0,0 +1,27 @@
+#include "AboutDialog.h"
+#include "../strings.h"
+#include "../version.h"
+
+AboutDialog::AboutDialog(QWidget* parent):
+ QMessageBox(parent)
+{
+ setIconPixmap(QPixmap(":/images/freshmemory.png"));
+ setWindowTitle(tr("About %1").arg( Strings::tr(Strings::s_appTitle)));
+ setText(createAboutText());
+ setEscapeButton(addButton(QMessageBox::Ok));
+}
+
+QString AboutDialog::createAboutText()
+{
+ QString formattedBuildStr;
+ if( !BuildStr.isEmpty() )
+ formattedBuildStr = "<p style=\"font-size:10pt\">" + BuildStr + "</p>";
+ return QString("<p><h2>") + Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "</h2></p>" +
+ formattedBuildStr +
+ "<p>" + tr("Learn new things quickly and keep your memory fresh with time spaced repetition.") + "</p>" +
+ "<p>" + Strings::tr(Strings::s_author) + "</p>" +
+ "<p><a href=\"http://fresh-memory.com\"> fresh-memory.com </a></p>" +
+ "</a></p>" +
+ "<p>" + tr("License:") + " <a href=\"http://www.gnu.org/copyleft/gpl.html\"> GPL 3" +
+ "</a></p>" + "<p><img src=\":/images/gplv3-88x31.png\"></p>";
+ }
diff --git a/src/main-view/AboutDialog.h b/src/main-view/AboutDialog.h
new file mode 100644
index 0000000..d05c11c
--- /dev/null
+++ b/src/main-view/AboutDialog.h
@@ -0,0 +1,17 @@
+#ifndef ABOUT_DIALOG_H
+#define ABOUT_DIALOG_H
+
+#include <QtWidgets>
+
+class AboutDialog: public QMessageBox
+{
+ Q_OBJECT
+
+public:
+ AboutDialog(QWidget* parent);
+
+private:
+ QString createAboutText();
+};
+
+#endif
diff --git a/src/main-view/AppModel.cpp b/src/main-view/AppModel.cpp
new file mode 100644
index 0000000..45e1956
--- /dev/null
+++ b/src/main-view/AppModel.cpp
@@ -0,0 +1,196 @@
+#include "AppModel.h"
+
+#include <stdlib.h>
+
+#include "../utils/RandomGenerator.h"
+#include "../dictionary/Dictionary.h"
+#include "../main-view/DictTableModel.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../dictionary/CardPack.h"
+
+AppModel::AppModel():
+ curDictIndex(-1), curCardPackIndex(0)
+{
+}
+
+AppModel::~AppModel()
+{
+ while(!dictionaries.isEmpty())
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeLast();
+ delete pair.second;
+ delete pair.first;
+ }
+}
+
+bool AppModel::openDictionary(const QString& filePath)
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ bool ok = dict->load(filePath);
+ if(ok)
+ addDictionary(dict);
+ else
+ {
+ errorMessage = dict->getErrorMessage();
+ delete dict;
+ }
+ return ok;
+}
+
+void AppModel::addDictionary( Dictionary* aDict )
+{
+ DictTableModel* dictModel = new DictTableModel( aDict );
+ dictionaries << qMakePair( aDict, dictModel );
+ curDictIndex = dictionaries.size() - 1;
+}
+
+Dictionary* AppModel::newDictionary()
+{
+ Dictionary* dict = new Dictionary( "", false, this );
+ dict->setDefaultFields();
+ addDictionary( dict );
+ return dict;
+}
+
+void AppModel::fixupCurDictIx()
+{
+ if( curDictIndex < 0 || curDictIndex >= dictionaries.size() )
+ curDictIndex = dictionaries.size()-1;
+}
+
+Dictionary* AppModel::curDictionary()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].first;
+ }
+
+DictTableModel* AppModel::curDictModel()
+ {
+ if( dictionaries.isEmpty() )
+ return NULL;
+ fixupCurDictIx();
+ return dictionaries[curDictIndex].second;
+ }
+
+Dictionary* AppModel::dictionary(int aIndex) const
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return NULL;
+ return dictionaries[aIndex].first;
+ }
+
+int AppModel::indexOfDictionary( Dictionary* aDic ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.first == aDic )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+int AppModel::indexOfDictionary( const QString& aFilePath ) const
+ {
+ for( int i = 0; i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ Q_ASSERT( pair.first );
+ if( pair.first->getFilePath() == aFilePath )
+ return i;
+ }
+ return -1; // Not found
+ }
+
+CardPack* AppModel::curCardPack()
+ {
+ Dictionary* curDic = curDictionary();
+ if( curDic )
+ return curDic->cardPack( curCardPackIndex );
+ else
+ return NULL;
+ }
+
+bool AppModel::setCurDictionary(int index)
+ {
+ if( dictionaries.isEmpty() )
+ {
+ curDictIndex = -1;
+ return false;
+ }
+ if( index < 0 || index >= dictionaries.size())
+ {
+ curDictIndex = dictionaries.size()-1;
+ return false;
+ }
+ curDictIndex = index;
+ return true;
+ }
+
+bool AppModel::removeDictionary( int aIndex )
+ {
+ if( aIndex < 0 || aIndex >= dictionaries.size() )
+ return false;
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.takeAt( aIndex );
+ delete pair.second;
+ delete pair.first;
+ return true;
+ }
+
+/** Destroys dic model and the dictionary itself.
+ */
+void AppModel::removeDictModel( QAbstractItemModel* aDictModel )
+ {
+ int i = 0;
+ for( ;i < dictionaries.size(); i++ )
+ {
+ QPair<Dictionary*, DictTableModel*> pair = dictionaries.at( i );
+ if( pair.second == aDictModel )
+ {
+ dictionaries.removeAt( i ); // Update the size of m_dictionaries before destroying
+ delete pair.first;
+ delete pair.second;
+ break;
+ }
+ }
+ }
+
+IStudyModel* AppModel::createStudyModel(int studyType, int cardPackIndex)
+ {
+ Dictionary* curDic = curDictionary();
+ if(!curDic)
+ {
+ errorMessage = tr("No dictionary opened.");
+ return NULL;
+ }
+ if(curDic->entriesNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ curCardPackIndex = cardPackIndex;
+ CardPack* cardPack = curDic->cardPack(curCardPackIndex);
+ if(!cardPack || cardPack->cardsNum() == 0)
+ {
+ errorMessage = tr("The current dictionary is empty.");
+ return NULL;
+ }
+
+ IStudyModel* studyModel = NULL;
+ switch(studyType)
+ {
+ case WordDrill:
+ studyModel = new WordDrillModel(cardPack);
+ break;
+ case SpacedRepetition:
+ studyModel = new SpacedRepetitionModel(cardPack, new RandomGenerator);
+ break;
+ }
+ if(studyModel)
+ studyModel->setDictModel( curDictModel() );
+ return studyModel;
+ }
diff --git a/src/main-view/AppModel.h b/src/main-view/AppModel.h
new file mode 100644
index 0000000..bf1ddd2
--- /dev/null
+++ b/src/main-view/AppModel.h
@@ -0,0 +1,64 @@
+#ifndef APPMODEL_H
+#define APPMODEL_H
+
+#include <QList>
+#include <QPair>
+#include <QFile>
+#include <QAbstractItemModel>
+
+class Dictionary;
+class DictTableModel;
+class CardPack;
+class IStudyModel;
+
+class AppModel: public QObject
+{
+Q_OBJECT
+
+public:
+ enum StudyType
+ {
+ WordDrill,
+ SpacedRepetition,
+ StudyTypesNum
+ };
+
+public:
+ AppModel();
+ ~AppModel();
+
+ bool openDictionary( const QString& filePath );
+ void addDictionary( Dictionary* aDict );
+ Dictionary* newDictionary();
+ Dictionary* curDictionary();
+ int getCurDictIndex() const { return curDictIndex; }
+ DictTableModel* curDictModel();
+ int dictionariesNum() const { return dictionaries.size(); }
+ Dictionary* dictionary(int aIndex) const;
+ int indexOfDictionary( Dictionary* aDic ) const;
+ int indexOfDictionary( const QString& aFilePath ) const;
+ // TODO: Synchronize this with the pack tree view selection
+ int curCardPackIx() const { return curCardPackIndex; }
+ CardPack* curCardPack();
+ QString getErrorMessage() const { return errorMessage; }
+
+public slots:
+ /** @return true, if the dictionary was successfully set. false, if there are no dictionaries, or the index is out of range.
+ * If the index is out of range, the last dictionary from the list is set.
+ */
+ bool setCurDictionary(int index);
+ bool removeDictionary(int aIndex);
+ void removeDictModel(QAbstractItemModel* aDictModel);
+ IStudyModel* createStudyModel(int studyType, int cardPackIndex);
+
+private:
+ void fixupCurDictIx();
+
+private:
+ QList< QPair<Dictionary*, DictTableModel*> > dictionaries;
+ QString errorMessage;
+ int curDictIndex;
+ int curCardPackIndex;
+};
+
+#endif
diff --git a/src/main-view/CardFilterModel.cpp b/src/main-view/CardFilterModel.cpp
new file mode 100644
index 0000000..50ac75d
--- /dev/null
+++ b/src/main-view/CardFilterModel.cpp
@@ -0,0 +1,24 @@
+#include "CardFilterModel.h"
+
+CardFilterModel::CardFilterModel( QObject* parent ):
+ QSortFilterProxyModel( parent )
+ {
+ }
+
+void CardFilterModel::addFilterRow( int aRow )
+ {
+ if( m_filterRows.contains( aRow ) )
+ return;
+ m_filterRows << aRow;
+ }
+
+void CardFilterModel::removeFilterRow( int aRow )
+ {
+ m_filterRows.removeOne( aRow );
+ }
+
+bool CardFilterModel::filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const
+ {
+ Q_UNUSED( source_parent );
+ return m_filterRows.contains( source_row );
+ }
diff --git a/src/main-view/CardFilterModel.h b/src/main-view/CardFilterModel.h
new file mode 100644
index 0000000..7c6e9f2
--- /dev/null
+++ b/src/main-view/CardFilterModel.h
@@ -0,0 +1,21 @@
+#ifndef CARDFILTERMODEL_H
+#define CARDFILTERMODEL_H
+
+#include <QSortFilterProxyModel>
+
+class CardFilterModel : public QSortFilterProxyModel
+ {
+ Q_OBJECT
+public:
+ CardFilterModel( QObject* parent );
+ void addFilterRow( int aRow );
+ void removeFilterRow( int aRow );
+
+protected:
+ bool filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const;
+
+private:
+ QList<int> m_filterRows;
+ };
+
+#endif // CARDFILTERMODEL_H
diff --git a/src/main-view/CardPreview.cpp b/src/main-view/CardPreview.cpp
new file mode 100644
index 0000000..aa59031
--- /dev/null
+++ b/src/main-view/CardPreview.cpp
@@ -0,0 +1,55 @@
+#include "CardPreview.h"
+#include "../study/CardSideView.h"
+#include "../dictionary/Card.h"
+
+CardPreview::CardPreview(QWidget* parent):
+ QDockWidget(parent)
+{
+ setDockProperties();
+ createCardSides();
+ createInternalWidget();
+}
+
+void CardPreview::setDockProperties()
+{
+ setWindowTitle(tr("Card preview"));
+ setObjectName("Card-preview");
+ setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+}
+
+void CardPreview::createCardSides()
+{
+ questionSide = new CardSideView(CardSideView::QstMode);
+ answerSide = new CardSideView(CardSideView::AnsMode);
+}
+
+void CardPreview::createInternalWidget()
+{
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(questionSide, 1);
+ mainLt->addWidget(answerSide, 1);
+
+ QWidget* internalWidget = new QWidget;
+ internalWidget->setLayout(mainLt);
+ setWidget(internalWidget);
+}
+
+void CardPreview::setPack(const CardPack* pack)
+{
+ questionSide->setPack(pack);
+ answerSide->setPack(pack);
+}
+
+void CardPreview::setContent(const CardPack* pack, Card* card)
+{
+ setPack(pack);
+ QString question;
+ QStringList answers;
+ if(card)
+ {
+ question = card->getQuestion();
+ answers = card->getAnswers();
+ }
+ questionSide->setQuestion(question);
+ answerSide->setQstAnsr(question, answers);
+}
diff --git a/src/main-view/CardPreview.h b/src/main-view/CardPreview.h
new file mode 100644
index 0000000..6217c96
--- /dev/null
+++ b/src/main-view/CardPreview.h
@@ -0,0 +1,30 @@
+#ifndef CARD_PREVIEW_H
+#define CARD_PREVIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class CardSideView;
+class Card;
+class CardPack;
+
+class CardPreview: public QDockWidget
+{
+ Q_OBJECT
+
+public:
+ CardPreview(QWidget* parent);
+ void setContent(const CardPack* pack, Card* card);
+
+private:
+ void createCardSides();
+ void createInternalWidget();
+ void setDockProperties();
+ void setPack(const CardPack* pack);
+
+private:
+ CardSideView* questionSide;
+ CardSideView* answerSide;
+};
+
+#endif
diff --git a/src/main-view/DictTableDelegate.cpp b/src/main-view/DictTableDelegate.cpp
new file mode 100644
index 0000000..1e1d609
--- /dev/null
+++ b/src/main-view/DictTableDelegate.cpp
@@ -0,0 +1,128 @@
+#include "DictTableDelegate.h"
+#include <QtDebug>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "DictTableDelegatePainter.h"
+#include "FieldContentCodec.h"
+#include "../dictionary/Dictionary.h"
+
+QWidget* DictTableDelegate::createEditor( QWidget* parent,
+ const QStyleOptionViewItem& option, const QModelIndex& /*index*/ ) const
+{
+ editor = new RecordEditor(parent, option.rect);
+ connect( editor, SIGNAL(destroyed()), SIGNAL(editorDestroyed()) );
+ emit editorCreated();
+ return editor;
+}
+
+void DictTableDelegate::updateEditorGeometry(QWidget* editor,
+ const QStyleOptionViewItem& /*option*/, const QModelIndex& index) const
+{
+ setEditorData(editor, index);
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ recordEditor->updateEditor();
+}
+
+bool DictTableDelegate::eventFilter(QObject *object, QEvent *event)
+{
+ QWidget* editor = qobject_cast<QWidget*>(object);
+ if (!editor)
+ return false;
+ if(event->type() == QEvent::KeyPress)
+ switch( static_cast<QKeyEvent*>(event)->key() )
+ {
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ case Qt::Key_Tab:
+ emit commitData(editor);
+ emit closeEditor(editor, QAbstractItemDelegate::EditNextItem);
+ return true;
+ default:
+ break;
+ }
+ return QStyledItemDelegate::eventFilter(object, event);
+}
+
+void DictTableDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
+{
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ FieldContentCodec codec(recordEditor);
+ codec.parse(getDisplayText(index));
+}
+
+QString DictTableDelegate::getDisplayText(const QModelIndex& index) const
+{
+ return index.data(Qt::EditRole).toString();
+}
+
+void DictTableDelegate::setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const
+ {
+ RecordEditor* recordEditor = qobject_cast<RecordEditor*>(editor);
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model );
+ QString editorText = recordEditor->getText();
+ QModelIndex origIndex = index;
+ if( !tableModel )
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model );
+ if( !proxyModel )
+ return;
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( !tableModel )
+ return;
+ origIndex = proxyModel->mapToSource( index );
+ }
+ if(editorText == getDisplayText(index) && !indexIsLastCell(origIndex, model))
+ return;
+ QString newText = tableModel->dictionary()->shortenImagePaths(editorText);
+ QUndoCommand* command = new EditRecordCmd( tableModel, origIndex, newText );
+ tableModel->undoStack()->push( command );
+}
+
+bool DictTableDelegate::indexIsLastCell(const QModelIndex& index,
+ QAbstractItemModel* model) const
+{
+ return index.row() == model->rowCount() - 1 &&
+ index.column() == model->columnCount() - 1;
+}
+
+void DictTableDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const
+{
+ QStyledItemDelegate::paint(painter, option, index);
+ DictTableDelegatePainter dPainter(painter, getMarginRect(option),
+ option.fontMetrics);
+ FieldContentCodec codec(&dPainter);
+ codec.parse(getDisplayText(index));
+}
+
+QRect DictTableDelegate::getMarginRect(const QStyleOptionViewItem& option) const
+{
+ const int margin = 2;
+ return option.rect.adjusted(margin, 0, -margin, 0);
+}
+
+int DictTableDelegate::getCursorPos() const
+{
+ if(editor)
+ return editor->textCursor().position();
+ else
+ return -1;
+}
+
+void DictTableDelegate::setCursorPos(int pos)
+{
+ if(!editor)
+ return;
+ QTextCursor cursor = editor->textCursor();
+ cursor.setPosition(pos);
+ editor->setTextCursor(cursor);
+}
+
+void DictTableDelegate::insertImageIntoEditor(int cursorPos, const QString& filePath)
+{
+ if(!editor)
+ return;
+ editor->insertImage(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableDelegate.h b/src/main-view/DictTableDelegate.h
new file mode 100644
index 0000000..cf43fc5
--- /dev/null
+++ b/src/main-view/DictTableDelegate.h
@@ -0,0 +1,44 @@
+#ifndef DICT_TABLE_DELEGATE_H
+#define DICT_TABLE_DELEGATE_H
+
+#include <QtWidgets>
+
+#include "RecordEditor.h"
+
+class DictTableDelegate: public QStyledItemDelegate
+{
+Q_OBJECT
+
+public:
+ DictTableDelegate(QObject* parent):
+ QStyledItemDelegate(parent), editor(NULL) {}
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ bool eventFilter(QObject *object, QEvent *event);
+ void setEditorData(QWidget* editor, const QModelIndex& index) const;
+ void setModelData(QWidget* editor, QAbstractItemModel* model,
+ const QModelIndex& index) const;
+ void paint(QPainter* painter, const QStyleOptionViewItem& option,
+ const QModelIndex& index) const;
+ int getCursorPos() const;
+ void setCursorPos(int pos);
+ void insertImageIntoEditor(int cursorPos, const QString& filePath);
+ QWidget* getEditor() const { return editor; }
+
+private:
+ QString getDisplayText(const QModelIndex& index) const;
+ QRect getMarginRect(const QStyleOptionViewItem& option) const;
+ bool indexIsLastCell(const QModelIndex& index, QAbstractItemModel* model) const;
+
+signals:
+ void editorCreated() const;
+ void editorDestroyed() const;
+
+private:
+ mutable RecordEditor* editor;
+};
+
+#endif
+
diff --git a/src/main-view/DictTableDelegatePainter.cpp b/src/main-view/DictTableDelegatePainter.cpp
new file mode 100644
index 0000000..2714bdc
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.cpp
@@ -0,0 +1,61 @@
+#include "DictTableDelegatePainter.h"
+
+DictTableDelegatePainter::DictTableDelegatePainter(QPainter* painter,
+ const QRect& contentRect, const QFontMetrics& fontMetrics):
+ painter(painter), contentRect(contentRect), fontMetrics(fontMetrics)
+{
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::initLoopParams()
+{
+ offset = 0;
+ textFitstoRect = true;
+}
+
+void DictTableDelegatePainter::startDrawing()
+{
+ painter->save();
+ painter->setClipRect(contentRect);
+ initLoopParams();
+}
+
+void DictTableDelegatePainter::endDrawing()
+{
+ painter->restore();
+}
+
+void DictTableDelegatePainter::drawText(const QString& text)
+{
+ if(!textFitstoRect)
+ return;
+ QString elidedText = getElidedText(text);
+ textFitstoRect = elidedText == text;
+ painter->drawText(contentRect.translated(offset, 0),
+ elidedText, getTextOption());
+ offset += fontMetrics.width(elidedText);
+}
+
+void DictTableDelegatePainter::drawImage(const QString& filePath)
+{
+ if(!textFitstoRect)
+ return;
+ QPixmap image(filePath);
+ if(image.isNull())
+ image = QPixmap(":/images/broken-image.png");
+ image = image.scaledToHeight(ThumbnailSize, Qt::SmoothTransformation);
+ painter->drawPixmap(contentRect.topLeft() + QPoint(offset, 0), image);
+ offset += image.width();
+}
+
+QString DictTableDelegatePainter::getElidedText(const QString& text)
+{
+ return fontMetrics.elidedText(text, Qt::ElideRight, contentRect.width() - offset);
+}
+
+QTextOption DictTableDelegatePainter::getTextOption()
+{
+ QTextOption textOption(Qt::AlignVCenter);
+ textOption.setWrapMode(QTextOption::NoWrap);
+ return textOption;
+}
diff --git a/src/main-view/DictTableDelegatePainter.h b/src/main-view/DictTableDelegatePainter.h
new file mode 100644
index 0000000..985ecac
--- /dev/null
+++ b/src/main-view/DictTableDelegatePainter.h
@@ -0,0 +1,36 @@
+#ifndef DICT_TABLE_DELEGATE_PAINTER_H
+#define DICT_TABLE_DELEGATE_PAINTER_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class DictTableDelegatePainter: public FieldContentPainter
+{
+private:
+ static QTextOption getTextOption();
+
+private:
+ static const int ThumbnailSize = 25;
+
+public:
+ DictTableDelegatePainter(QPainter* painter, const QRect& contentRect,
+ const QFontMetrics& fontMetrics);
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+
+private:
+ void initLoopParams();
+ QString getElidedText(const QString& text);
+
+private:
+ QPainter* painter;
+ QRect contentRect;
+ QFontMetrics fontMetrics;
+ int offset;
+ bool textFitstoRect;
+};
+#endif
+
diff --git a/src/main-view/DictTableModel.cpp b/src/main-view/DictTableModel.cpp
new file mode 100644
index 0000000..b7b7eed
--- /dev/null
+++ b/src/main-view/DictTableModel.cpp
@@ -0,0 +1,142 @@
+#include <QtGui>
+
+#include "DictTableModel.h"
+#include "UndoCommands.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/Field.h"
+
+DictTableModel::DictTableModel( Dictionary* aDict, QObject *parent ):
+ QAbstractTableModel( parent ), m_dictionary( aDict )
+ {
+ m_undoStack = new QUndoStack( this );
+ connect( m_dictionary, SIGNAL(destroyed(QObject*)), SLOT(discardCurDictionary()) );
+ }
+
+int DictTableModel::rowCount( const QModelIndex &/*parent*/ ) const
+ {
+ if( m_dictionary )
+ return m_dictionary->entriesNum();
+ else
+ return 0;
+ }
+
+int DictTableModel::columnCount(const QModelIndex &/*parent*/) const
+ {
+ if( m_dictionary )
+ return m_dictionary->fieldsNum();
+ else
+ return 0;
+ }
+
+QVariant DictTableModel::data( const QModelIndex &index, int role ) const
+ {
+ if( !m_dictionary || !index.isValid() || index.row() >= rowCount() ||
+ index.column() >= columnCount() )
+ return QVariant();
+ QString text = m_dictionary->getFieldValue(index.row(), index.column());
+ switch(role)
+ {
+ case Qt::DisplayRole:
+ return QVariant();
+ case Qt::EditRole:
+ return m_dictionary->extendImagePaths(text);
+ case DicRecordRole:
+ {
+ const DicRecord* record = m_dictionary->getRecord(index.row());
+ return QVariant::fromValue(*record);
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+QVariant DictTableModel::headerData( int section, Qt::Orientation orientation, int role ) const
+ {
+ if( !m_dictionary )
+ return QVariant();
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ if(orientation == Qt::Vertical)
+ return QString::number( section + 1 );
+ else
+ {
+ if( section < columnCount() )
+ return m_dictionary->field( section )->name();
+ else
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
+
+bool DictTableModel::setData( const QModelIndex &index, const QVariant &value, int role )
+ {
+ if( !index.isValid() || !m_dictionary )
+ return false;
+ if( value == data( index, role ) )
+ return false;
+
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ m_dictionary->setFieldValue(index.row(), index.column(), value.toString());
+ break;
+ case DicRecordRole:
+ {
+ DicRecord record = value.value<DicRecord>();
+ m_dictionary->setRecord( index.row(), record );
+ break;
+ }
+ default:
+ return false;
+ }
+ emit dataChanged( index, index );
+ return true;
+ }
+
+bool DictTableModel::insertRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->insertEntries( position, rows );
+ endInsertRows();
+ return true;
+ }
+
+bool DictTableModel::addFields( QStringList aFields )
+ {
+ if( !m_dictionary )
+ return false;
+ beginInsertColumns( QModelIndex(), columnCount(), columnCount() + aFields.size() - 1 );
+ m_dictionary->addFields( aFields );
+ endInsertColumns();
+ return true;
+ }
+
+bool DictTableModel::removeRows( int position, int rows, const QModelIndex& /*parent*/ )
+ {
+ if( !m_dictionary )
+ return false;
+ beginRemoveRows( QModelIndex(), position, position + rows - 1 );
+ m_dictionary->removeRecords( position, rows );
+ endRemoveRows();
+ return true;
+ }
+
+void DictTableModel::resetData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+void DictTableModel::discardCurDictionary()
+ {
+ beginResetModel();
+ m_dictionary = NULL;
+ endResetModel();
+ }
diff --git a/src/main-view/DictTableModel.h b/src/main-view/DictTableModel.h
new file mode 100644
index 0000000..0293ace
--- /dev/null
+++ b/src/main-view/DictTableModel.h
@@ -0,0 +1,52 @@
+#ifndef DICTTABLEMODEL_H
+#define DICTTABLEMODEL_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class Dictionary;
+class DicRecord;
+
+class DictTableModel: public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum
+ {
+ DicRecordRole = Qt::UserRole
+ };
+
+ DictTableModel( Dictionary* aDict, QObject *parent = 0 );
+
+ // Getters
+
+ const Dictionary* dictionary() const { return m_dictionary; }
+ QUndoStack* undoStack() const { return m_undoStack; }
+
+ Qt::ItemFlags flags( const QModelIndex& index ) const { return QAbstractItemModel::flags( index ) | Qt::ItemIsEditable; }
+ int rowCount( const QModelIndex& parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex& parent = QModelIndex() ) const;
+ QVariant data(const QModelIndex& index, int role) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+
+ // Setters
+
+ bool setData( const QModelIndex &index, const QVariant& value, int role = Qt::EditRole );
+ bool insertRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+ bool addFields( QStringList aFields );
+ bool removeRows( int position, int rows, const QModelIndex& index = QModelIndex() );
+
+public slots:
+ void resetData();
+
+private slots:
+ void discardCurDictionary();
+
+private:
+ static const int ThumbnailSize = 25; ///< Size of image thumbnails
+
+ Dictionary* m_dictionary; // not own
+ QUndoStack* m_undoStack;
+};
+#endif
diff --git a/src/main-view/DictTableView.cpp b/src/main-view/DictTableView.cpp
new file mode 100644
index 0000000..9d61fad
--- /dev/null
+++ b/src/main-view/DictTableView.cpp
@@ -0,0 +1,84 @@
+#include "DictTableView.h"
+#include "DictTableDelegate.h"
+
+#include <QtAlgorithms>
+#include <QHeaderView>
+#include <QAbstractProxyModel>
+
+#include "DictTableModel.h"
+
+DictTableView::DictTableView( QAbstractItemModel* aModel, QWidget* aParent ):
+ QTableView( aParent )
+ {
+ setModel( aModel );
+ connect( aModel, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)),
+ SLOT(disableUpdates()) );
+ connect( aModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
+ SLOT(enableUpdates()) );
+ setItemDelegate( new DictTableDelegate(this) );
+ setAlternatingRowColors(true);
+ setShowGrid(true);
+ verticalHeader()->setDefaultSectionSize( RowHeight );
+ setSelectionBehavior( QAbstractItemView::SelectRows );
+ resizeColumnsToContents();
+ horizontalHeader()->stretchLastSection();
+ }
+
+DictTableView::~DictTableView()
+ {
+ emit destroyed( model() );
+ }
+
+DictTableModel* DictTableView::dicTableModel() const
+ {
+ DictTableModel* tableModel = qobject_cast<DictTableModel*>( model() );
+ if( tableModel )
+ return tableModel;
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( model() );
+ if( proxyModel )
+ {
+ tableModel = qobject_cast<DictTableModel*>( proxyModel->sourceModel() );
+ if( tableModel )
+ return tableModel;
+ }
+ return NULL;
+ }
+
+void DictTableView::resizeColumnsToContents()
+ {
+ QTableView::resizeColumnsToContents();
+ for( int i=0; i<model()->columnCount(); i++ )
+ if( columnWidth( i ) < KMinColWidth )
+ setColumnWidth( i, KMinColWidth );
+ else if ( columnWidth( i ) > KMaxColWidth )
+ setColumnWidth( i, KMaxColWidth );
+}
+
+void DictTableView::startEditing(int row, int col)
+{
+ edit(model()->index(row, col));
+}
+
+void DictTableView::commitEditing()
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ commitData(delegate->getEditor());
+}
+
+int DictTableView::getEditorCursorPos() const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return -1;
+ return delegate->getCursorPos();
+}
+
+void DictTableView::insertImageIntoEditor(int cursorPos, const QString& filePath) const
+{
+ DictTableDelegate* delegate = qobject_cast<DictTableDelegate*>(itemDelegate());
+ if(!delegate)
+ return;
+ delegate->insertImageIntoEditor(cursorPos, filePath);
+}
diff --git a/src/main-view/DictTableView.h b/src/main-view/DictTableView.h
new file mode 100644
index 0000000..b34df65
--- /dev/null
+++ b/src/main-view/DictTableView.h
@@ -0,0 +1,36 @@
+#ifndef DICTTABLEVIEW_h
+#define DICTTABLEVIEW_h
+
+#include <QTableView>
+#include <QtDebug>
+
+class DictTableModel;
+
+class DictTableView: public QTableView
+{
+Q_OBJECT
+
+public:
+ DictTableView( QAbstractItemModel* aModel, QWidget* aParent = 0 );
+ virtual ~DictTableView();
+ void resizeColumnsToContents();
+ DictTableModel* dicTableModel() const;
+ void startEditing(int row, int col);
+ void commitEditing();
+ int getEditorCursorPos() const;
+ void insertImageIntoEditor(int cursorPos, const QString& filePath) const;
+
+private slots:
+ void enableUpdates() { setUpdatesEnabled(true); }
+ void disableUpdates() { setUpdatesEnabled(false); }
+
+signals:
+ void destroyed( QAbstractItemModel* aDictModel );
+
+private:
+ static const int KMinColWidth = 170;
+ static const int KMaxColWidth = 400;
+ static const int RowHeight = 27;
+};
+
+#endif
diff --git a/src/main-view/DictionaryTabWidget.cpp b/src/main-view/DictionaryTabWidget.cpp
new file mode 100644
index 0000000..94f090c
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.cpp
@@ -0,0 +1,108 @@
+#include "DictionaryTabWidget.h"
+#include "MainWindow.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+
+DictionaryTabWidget::DictionaryTabWidget(MainWindow* aMainWin):
+ QTabWidget( aMainWin ), m_mainWin( aMainWin ), createdEditorsNum(0)
+ {
+ setDocumentMode( true );
+ setTabsClosable( true );
+ setMovable( true );
+ connect( this, SIGNAL(tabCloseRequested(int)), SLOT(closeTab(int)) );
+ m_undoGroup = new QUndoGroup( this );
+
+ m_continueLbl = new QLabel( this );
+ m_continueLbl->hide();
+ m_continueLbl->setPixmap(QPixmap(":/images/continue-search.png"));
+ }
+
+
+/** The tab is removed automatically when the widget is destroyed.
+ The undo stack is removed from group automatically, when the stack is destroyed.
+ */
+void DictionaryTabWidget::closeTab( int aIndex )
+ {
+ if( aIndex == -1 )
+ aIndex = currentIndex();
+ bool canRemove = m_mainWin->proposeToSave( aIndex );
+ if( canRemove )
+ delete widget( aIndex );
+ }
+
+int DictionaryTabWidget::addDictTab( DictTableModel* aDictModel )
+ {
+ DictTableView* dictView = new DictTableView( aDictModel );
+ int tabIx = addTab( dictView, "" );
+ setCurrentIndex( tabIx );
+ QUndoStack* undoStack = aDictModel->undoStack();
+ m_undoGroup->addStack( undoStack );
+ m_undoGroup->setActiveStack( undoStack );
+ connect( undoStack, SIGNAL(cleanChanged(bool)), aDictModel->dictionary(), SLOT(setContentClean(bool)) );
+ connect( dictView->itemDelegate(), SIGNAL(editorCreated()), SLOT(createEditor()) );
+ connect( dictView->itemDelegate(), SIGNAL(editorDestroyed()), SLOT(destroyEditor()) );
+ return tabIx;
+ }
+
+const DictTableView* DictionaryTabWidget::curDictView() const
+ {
+ QWidget* curWidget = currentWidget();
+ if( !curWidget )
+ return NULL;
+ DictTableView* curView = static_cast<DictTableView*>( curWidget );
+ return curView;
+ }
+
+void DictionaryTabWidget::setCurrentIndex( int aTabIx )
+ {
+ QTabWidget::setCurrentIndex( aTabIx );
+ const DictTableView* dictView = curDictView();
+ if( dictView )
+ {
+ const DictTableModel* dictModel = dictView->dicTableModel();
+ if( dictModel )
+ m_undoGroup->setActiveStack( dictModel->undoStack() );
+ }
+ }
+
+void DictionaryTabWidget::goToDictionaryRecord( int aDictIx, int aRecordRow )
+ {
+ setCurrentIndex( aDictIx );
+ QWidget* curWidget = currentWidget();
+ Q_ASSERT( curWidget );
+ QAbstractItemView* curView = static_cast<QAbstractItemView*>( curWidget );
+ Q_ASSERT( curView );
+ curView->setFocus();
+ QModelIndex index = curView->model()->index( aRecordRow, 0 );
+ curView->setCurrentIndex( index );
+ }
+
+void DictionaryTabWidget::cleanUndoStack()
+{
+ m_undoGroup->activeStack()->setClean();
+}
+
+bool DictionaryTabWidget::undoStackIsClean() const
+{
+ return m_undoGroup->isClean();
+}
+
+void DictionaryTabWidget::showContinueSearch()
+{
+ m_continueLbl->move( rect().center() - m_continueLbl->rect().center() );
+ m_continueLbl->show();
+ QTimer::singleShot( 500, m_continueLbl, SLOT(hide()) );
+}
+
+void DictionaryTabWidget::createEditor()
+{
+ createdEditorsNum++;
+ emit editingStateChanged();
+}
+
+void DictionaryTabWidget::destroyEditor()
+{
+ createdEditorsNum--;
+ emit editingStateChanged();
+}
diff --git a/src/main-view/DictionaryTabWidget.h b/src/main-view/DictionaryTabWidget.h
new file mode 100644
index 0000000..b5bd3c7
--- /dev/null
+++ b/src/main-view/DictionaryTabWidget.h
@@ -0,0 +1,46 @@
+#ifndef DICTIONARYTABWIDGET_H
+#define DICTIONARYTABWIDGET_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class MainWindow;
+class FilterBar;
+class DictTableModel;
+class DictTableView;
+class Dictionary;
+
+class DictionaryTabWidget : public QTabWidget
+{
+ Q_OBJECT
+public:
+ DictionaryTabWidget(MainWindow* aMainWin);
+
+ int addDictTab( DictTableModel* aDictModel );
+ void setCurrentIndex( int aTabIx );
+ void goToDictionaryRecord( int aDictIx, int aRecordRow );
+ void cleanUndoStack();
+ void showContinueSearch();
+
+ const DictTableView* curDictView() const;
+ QUndoGroup* undoGroup() const { return m_undoGroup; }
+ bool undoStackIsClean() const;
+ bool isInEditingState() const { return createdEditorsNum > 0; }
+
+public slots:
+ void closeTab( int aIndex = -1 );
+
+private slots:
+ void createEditor();
+ void destroyEditor();
+
+signals:
+ void editingStateChanged();
+
+private:
+ MainWindow* m_mainWin; // parent, now own
+ QUndoGroup* m_undoGroup;
+ QLabel* m_continueLbl; // Continue search icon
+ int createdEditorsNum;
+};
+#endif // DICTIONARYTABWIDGET_H
diff --git a/src/main-view/FieldContentCodec.cpp b/src/main-view/FieldContentCodec.cpp
new file mode 100644
index 0000000..ac9e6c5
--- /dev/null
+++ b/src/main-view/FieldContentCodec.cpp
@@ -0,0 +1,52 @@
+#include "FieldContentCodec.h"
+
+#include "FieldContentPainter.h"
+
+FieldContentCodec::FieldContentCodec(FieldContentPainter* painter):
+ painter(painter),
+ imageRx("<img\\s+src=\"([^\"]+)\"\\s*/?>")
+{
+ initLoopParams();
+}
+
+void FieldContentCodec::initLoopParams()
+{
+ textPos = 0;
+ prevTextPos = 0;
+}
+
+void FieldContentCodec::parse(const QString& text)
+{
+ painter->startDrawing();
+ initLoopParams();
+ this->text = text;
+
+ while(findNextImage() >= 0)
+ {
+ drawTextChunk(textPos - prevTextPos);
+ drawImage();
+ textPos += imageRx.matchedLength();
+ prevTextPos = textPos;
+ }
+ drawTextChunk(-1);
+ painter->endDrawing();
+}
+
+int FieldContentCodec::findNextImage()
+{
+ textPos = imageRx.indexIn(text, textPos);
+ return textPos;
+}
+
+void FieldContentCodec::drawTextChunk(int len)
+{
+ painter->drawText(text.mid(prevTextPos, len));
+}
+
+void FieldContentCodec::drawImage()
+{
+ QString imagePath = imageRx.cap(1);
+ if(!QFileInfo(imagePath).exists())
+ imagePath = ":/images/broken-image.png";
+ painter->drawImage(imagePath);
+}
diff --git a/src/main-view/FieldContentCodec.h b/src/main-view/FieldContentCodec.h
new file mode 100644
index 0000000..8a13dba
--- /dev/null
+++ b/src/main-view/FieldContentCodec.h
@@ -0,0 +1,28 @@
+#ifndef FIELD_CONTENT_CODEC_H
+#define FIELD_CONTENT_CODEC_H
+
+#include <QtCore>
+
+class FieldContentPainter;
+
+class FieldContentCodec
+{
+public:
+ FieldContentCodec(FieldContentPainter* painter);
+ void parse(const QString& text);
+
+private:
+ void initLoopParams();
+ int findNextImage();
+ void drawTextChunk(int len);
+ void drawImage();
+
+private:
+ FieldContentPainter* painter;
+ QString text;
+ QRegExp imageRx;
+ int textPos;
+ int prevTextPos;
+};
+#endif
+
diff --git a/src/main-view/FieldContentPainter.h b/src/main-view/FieldContentPainter.h
new file mode 100644
index 0000000..eb0679a
--- /dev/null
+++ b/src/main-view/FieldContentPainter.h
@@ -0,0 +1,16 @@
+#ifndef FIELD_CONTENT_PAINTER_H
+#define FIELD_CONTENT_PAINTER_H
+
+#include <QtCore>
+
+class FieldContentPainter
+{
+public:
+ virtual ~FieldContentPainter() {}
+ virtual void startDrawing() = 0;
+ virtual void endDrawing() = 0;
+ virtual void drawText(const QString& text) = 0;
+ virtual void drawImage(const QString& filePath) = 0;
+};
+#endif
+
diff --git a/src/main-view/FindPanel.cpp b/src/main-view/FindPanel.cpp
new file mode 100644
index 0000000..755fa50
--- /dev/null
+++ b/src/main-view/FindPanel.cpp
@@ -0,0 +1,258 @@
+#include "FindPanel.h"
+
+#include <QHBoxLayout>
+#include <QAction>
+#include <QLineEdit>
+#include <QKeyEvent>
+
+#include "DictTableView.h"
+#include "MainWindow.h"
+
+FindPanel::FindPanel( MainWindow* aMainWindow ):
+ QWidget( aMainWindow ),
+ m_mainWindow( aMainWindow ), m_direction( 1 ), m_foundOnce(false)
+ {
+ m_closeButton = new QToolButton;
+ m_closeButton->setAutoRaise(true);
+ m_closeButton->setIcon(QIcon(":/images/gray-cross.png"));
+ m_closeButton->setToolTip(tr("Close"));
+
+ QLabel* textLabel = new QLabel(tr("Find:", "Title of the find pane"));
+
+ m_textEdit = new QComboBox;
+ textLabel->setBuddy(m_textEdit);
+ m_textEdit->setEditable(true);
+ m_textEdit->setInsertPolicy( QComboBox::NoInsert );
+ m_textEdit->setMaxCount( ComboBoxMaxItems );
+ m_textEdit->setMinimumWidth( TextEditMinWidth );
+ m_textEdit->setFocus();
+
+ m_findBackwardBtn = new QToolButton;
+ m_findBackwardBtn->resize(32, 32);
+ m_findBackwardBtn->setObjectName("backward");
+ m_findBackwardBtn->setIcon(QIcon(":/images/1leftarrow.png"));
+ m_findBackwardBtn->setToolTip(tr("Find previous"));
+ m_findBackwardBtn->setEnabled(false);
+
+ m_findForwardBtn = new QToolButton;
+ m_findForwardBtn->setObjectName("forward");
+ m_findForwardBtn->setIcon(QIcon(":/images/1rightarrow.png"));
+ m_findForwardBtn->setToolTip(tr("Find next"));
+ m_findForwardBtn->setEnabled(false);
+
+ m_caseSensitiveBtn = new QToolButton;
+ m_caseSensitiveBtn->setAutoRaise(true);
+ m_caseSensitiveBtn->setCheckable( true );
+ m_caseSensitiveBtn->setIcon(QIcon(":/images/Aa.png"));
+ m_caseSensitiveBtn->setToolTip(tr("Case sensitive"));
+
+ m_wholeWordsBtn = new QToolButton;
+ m_wholeWordsBtn->setAutoRaise(true);
+ m_wholeWordsBtn->setCheckable( true );
+ m_wholeWordsBtn->setIcon(QIcon(":/images/whole-words.png"));
+ m_wholeWordsBtn->setToolTip(tr("Whole words"));
+
+ m_regExpBtn = new QToolButton;
+ m_regExpBtn->setAutoRaise(true);
+ m_regExpBtn->setCheckable( true );
+ m_regExpBtn->setIcon(QIcon(":/images/RX.png"));
+ m_regExpBtn->setToolTip(tr("Regular expression"));
+
+ m_inSelectionBtn = new QToolButton;
+ m_inSelectionBtn->setAutoRaise(true);
+ m_inSelectionBtn->setCheckable( true );
+ m_inSelectionBtn->setIcon(QIcon(":/images/selection.png"));
+ m_inSelectionBtn->setToolTip(tr("In selection"));
+
+ QLabel* infoIconLbl = new QLabel;
+ infoIconLbl->setPixmap( QPixmap(":/images/warning.png").scaled(
+ 16, 16, Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
+ m_infoLbl = new QLabel(tr("String is not found"));
+ QHBoxLayout* infoLt = new QHBoxLayout;
+ infoLt->setContentsMargins( QMargins() );
+ infoLt->addWidget( infoIconLbl );
+ infoLt->addWidget( m_infoLbl );
+ m_infoPane = new QWidget;
+ m_infoPane->setLayout( infoLt );
+ m_infoPane->hide();
+
+ QHBoxLayout* mainLayout = new QHBoxLayout;
+ mainLayout->addWidget( m_closeButton );
+ mainLayout->addWidget( textLabel );
+ mainLayout->addWidget( m_textEdit );
+ mainLayout->addWidget( m_findBackwardBtn );
+ mainLayout->addWidget( m_findForwardBtn );
+ mainLayout->addWidget( m_caseSensitiveBtn );
+ mainLayout->addWidget( m_wholeWordsBtn );
+ mainLayout->addWidget( m_regExpBtn );
+ mainLayout->addWidget( m_inSelectionBtn );
+ mainLayout->addSpacing( 50 );
+ mainLayout->addWidget( m_infoPane );
+ mainLayout->addStretch();
+ mainLayout->setContentsMargins( QMargins() );
+ setLayout( mainLayout );
+
+ connect( m_inSelectionBtn, SIGNAL(toggled(bool)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), SLOT(updateFindButtons()) );
+ connect( m_textEdit, SIGNAL(editTextChanged(const QString&)), m_infoPane, SLOT(hide()) );
+ connect( m_findForwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_findBackwardBtn, SIGNAL(clicked()), this, SLOT(find()) );
+ connect( m_closeButton, SIGNAL(clicked()), this, SLOT(hide()) );
+ }
+
+void FindPanel::show()
+ {
+ m_textEdit->setFocus();
+ m_textEdit->lineEdit()->selectAll();
+ QWidget::show();
+ }
+
+void FindPanel::keyPressEvent( QKeyEvent* event )
+ {
+ if( event->key() == Qt::Key_Return )
+ find();
+ else
+ QWidget::keyPressEvent( event );
+ }
+
+/**
+ @arg aDirection 1 = forward; -1 = backward; 0 = previous direction
+ */
+void FindPanel::find()
+ {
+ // Check controls
+ QString searchText = m_textEdit->currentText();
+ if( searchText.isEmpty() )
+ return;
+
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndexList rows = tableView->selectionModel()->selectedRows();
+ if( rows.size() <= 1 && m_inSelectionBtn->isChecked() )
+ m_inSelectionBtn->setChecked( false );
+
+ // Process search parameters
+ bool inSelection = m_inSelectionBtn->isChecked();
+ bool fromCursor = true;
+ if( inSelection || (rows.size() == 1 && rows.first().row() == 0 ) )
+ fromCursor = false;
+
+ // Process direction
+ if( sender() )
+ {
+ if( sender()->objectName() == "forward" )
+ m_direction = 1;
+ else if( sender()->objectName() == "backward" )
+ m_direction = -1;
+ }
+ // For "find again" case, the direction stays the same
+
+ // Save the entered text to combobox
+ int textIndex = m_textEdit->findText( searchText );
+ if( textIndex != 0 ) // Don't re-add the same text at the first line
+ {
+ if( textIndex > -1 ) // Remove duplicates
+ m_textEdit->removeItem( textIndex );
+ m_textEdit->insertItem( 0, searchText );
+ }
+
+ // Create the search regular expression
+ QString searchRegExpStr;
+ if( !m_regExpBtn->isChecked() )
+ searchRegExpStr = QRegExp::escape( searchText );
+ else
+ searchRegExpStr = searchText;
+ if( m_wholeWordsBtn->isChecked() )
+ searchRegExpStr = "\\b" + searchText + "\\b";
+
+ QRegExp searchRegExp = QRegExp( searchRegExpStr,
+ m_caseSensitiveBtn->isChecked()? Qt::CaseSensitive : Qt::CaseInsensitive);
+
+ // Get sorted search range
+ QModelIndexList searchRange;
+ if( inSelection )
+ {
+ searchRange = tableView->selectionModel()->selectedIndexes();
+ qSort( searchRange );
+ }
+ else // all indexes
+ {
+ QAbstractItemModel* tableModel = tableView->model();
+ for(int r=0; r < tableModel->rowCount(); r++)
+ for(int c=0; c < tableModel->columnCount(); c++)
+ searchRange << tableModel->index(r, c);
+ }
+
+ // Get the starting search point (iterator)
+ QListIterator<QModelIndex> startingPoint( searchRange );
+ if( fromCursor )
+ {
+ bool ok = startingPoint.findNext( tableView->currentIndex() );
+ if( !ok )
+ startingPoint.toFront();
+ if( ok && m_direction < 0 )
+ startingPoint.previous(); // Go one item backwards
+ }
+ else
+ if( m_direction < 0 )
+ startingPoint.toBack();
+
+ // Try to find the regexp
+ bool found = findRegExp( searchRegExp, startingPoint );
+ if ( !found && fromCursor )
+ {
+ // Continue searching
+ m_mainWindow->showContinueSearch();
+ if( m_direction > 0 )
+ startingPoint.toFront();
+ else
+ startingPoint.toBack();
+ if( findRegExp( searchRegExp, startingPoint ) )
+ found = true;
+ }
+ if( !found )
+ m_infoPane->show();
+ }
+
+bool FindPanel::findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint )
+ {
+ DictTableView* tableView = const_cast<DictTableView*>( m_mainWindow->getCurDictView() );
+ QModelIndex foundIndex;
+ while( (m_direction > 0)? aStartingPoint.hasNext() : aStartingPoint.hasPrevious() )
+ {
+ QModelIndex index = (m_direction > 0)? aStartingPoint.next() : aStartingPoint.previous();
+ QString valueStr = index.data( Qt::EditRole ).toString(); // Search in display, not edit, strings. Matters for <img>.
+ if( valueStr.contains( aSearchRegExp ) )
+ {
+ foundIndex = index;
+ break;
+ }
+ }
+ if( foundIndex.isValid() )
+ {
+ tableView->setFocus();
+ tableView->setCurrentIndex( foundIndex );
+ return true;
+ }
+ else
+ return false;
+ }
+
+void FindPanel::updateFindButtons()
+ {
+ m_findForwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() );
+ m_findBackwardBtn->setEnabled( !m_textEdit->currentText().isEmpty() && !m_inSelectionBtn->isChecked() );
+ }
+
+bool FindPanel::canFindAgain()
+ {
+ if( !m_foundOnce )
+ return false;
+ const DictTableView* tableView = m_mainWindow->getCurDictView();
+ if( tableView && !m_textEdit->currentText().isEmpty() )
+ return true;
+ else
+ return false;
+ }
+
+
+
diff --git a/src/main-view/FindPanel.h b/src/main-view/FindPanel.h
new file mode 100644
index 0000000..553522e
--- /dev/null
+++ b/src/main-view/FindPanel.h
@@ -0,0 +1,66 @@
+#ifndef FINDPANEL_H
+#define FINDPANEL_H
+
+#include <QWidget>
+#include <QString>
+#include <QModelIndex>
+#include <QComboBox>
+#include <QPushButton>
+#include <QToolButton>
+#include <QCheckBox>
+#include <QMenu>
+#include <QLabel>
+
+class DictTableView;
+class MainWindow;
+
+/**
+ * Modern find panel for dictionary records.
+ */
+
+class FindPanel: public QWidget
+{
+ Q_OBJECT
+public:
+ FindPanel( MainWindow* aMainWindow );
+
+ void setTableView( DictTableView* aTableView );
+ bool canFindAgain();
+
+protected:
+ void keyPressEvent( QKeyEvent* event );
+
+private:
+ bool findRegExp( const QRegExp& aSearchRegExp, QListIterator<QModelIndex> aStartingPoint );
+
+public slots:
+ void show();
+ void find();
+
+private slots:
+ void updateFindButtons();
+
+private:
+ static const int ComboBoxMaxItems = 10;
+ static const int TextEditMinWidth = 300;
+
+private:
+ // Data
+ MainWindow* m_mainWindow;
+ int m_direction; // Search direction: 1 or -1
+ bool m_foundOnce; // The search text was already found once. For "Find again".
+
+ // GUI
+ QToolButton* m_closeButton;
+ QComboBox* m_textEdit;
+ QToolButton* m_findForwardBtn;
+ QToolButton* m_findBackwardBtn;
+ QToolButton* m_caseSensitiveBtn;
+ QToolButton* m_wholeWordsBtn;
+ QToolButton* m_regExpBtn;
+ QToolButton* m_inSelectionBtn;
+ QWidget* m_infoPane;
+ QLabel* m_infoLbl;
+};
+
+#endif
diff --git a/src/main-view/LanguageMenu.cpp b/src/main-view/LanguageMenu.cpp
new file mode 100644
index 0000000..84ffd46
--- /dev/null
+++ b/src/main-view/LanguageMenu.cpp
@@ -0,0 +1,64 @@
+#include "LanguageMenu.h"
+#include "../main.h"
+#include "../strings.h"
+
+LanguageMenu::LanguageMenu(QMenu* parent):
+ QMenu(tr("&Language"), parent),
+ locale(QSettings().value("lang").toString()),
+ systemLocale(QLocale::system().name().split("_").first())
+{
+ initUi(parent);
+ initLangs();
+ createActionsGroup();
+ foreach(QString key, langs.keys())
+ createAction(key);
+}
+
+void LanguageMenu::initUi(QMenu* parent)
+{
+ parent->addMenu(this);
+ setIcon(QIcon(":/images/language.png"));
+}
+
+void LanguageMenu::initLangs()
+{
+ langs["cs"] = "Čeština (Czech)";
+ langs["fi"] = "Suomi (Finnish)";
+ langs["fr"] = "Français (French)";
+ langs["de"] = "Deutsch (German)";
+ langs["en"] = "English";
+ langs["ru"] = "Русский (Russian)";
+ langs["es"] = "Español (Spanish)";
+ langs["uk"] = "Українська (Ukrainian)";
+ langs[""] = tr("System") + ": " + langs[systemLocale];
+}
+
+void LanguageMenu::createActionsGroup()
+{
+ actionsGroup = new QActionGroup(this);
+ actionsGroup->setExclusive(true);
+}
+
+void LanguageMenu::createAction(const QString& key)
+{
+ QAction* action = addAction(langs[key]);
+ action->setCheckable(true);
+ if(key == locale)
+ action->setChecked(true);
+ action->setData(key);
+ actionsGroup->addAction(action);
+ connect(action, SIGNAL(triggered()), SLOT(saveLanguage()));
+}
+
+void LanguageMenu::saveLanguage()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ QString newLocale = action->data().toString();
+ QSettings().setValue("lang", newLocale);
+ if(newLocale.isEmpty())
+ newLocale = systemLocale;
+
+ installTranslator("freshmemory_" + newLocale, getResourcePath() + "/tr");
+ QMessageBox::information(this, Strings::tr(Strings::s_appTitle),
+ tr("The application must be restarted to use the selected language"));
+}
diff --git a/src/main-view/LanguageMenu.h b/src/main-view/LanguageMenu.h
new file mode 100644
index 0000000..d03984d
--- /dev/null
+++ b/src/main-view/LanguageMenu.h
@@ -0,0 +1,28 @@
+#ifndef LANGUAGE_MENU_H
+#define LANGUAGE_MENU_H
+
+#include <QtWidgets>
+
+class LanguageMenu: public QMenu
+{
+ Q_OBJECT
+public:
+ LanguageMenu(QMenu* parent);
+
+private:
+ void initUi(QMenu* parent);
+ void initLangs();
+ void createActionsGroup();
+ void createAction(const QString& key);
+
+private slots:
+ void saveLanguage();
+
+private:
+ QMap<QString, QString> langs;
+ QString locale;
+ QString systemLocale;
+ QActionGroup* actionsGroup;
+};
+
+#endif
diff --git a/src/main-view/MainWindow.cpp b/src/main-view/MainWindow.cpp
new file mode 100644
index 0000000..e5b285c
--- /dev/null
+++ b/src/main-view/MainWindow.cpp
@@ -0,0 +1,1313 @@
+#include "MainWindow.h"
+#include "AppModel.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "FindPanel.h"
+#include "DictTableDelegate.h"
+#include "PacksTreeModel.h"
+#include "AboutDialog.h"
+#include "RecentFilesManager.h"
+#include "LanguageMenu.h"
+#include "CardPreview.h"
+#include "WelcomeScreen.h"
+#include "../version.h"
+#include "../strings.h"
+#include "../dictionary/DicRecord.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/DicCsvWriter.h"
+#include "../dictionary/DicCsvReader.h"
+#include "../dic-options/DictionaryOptionsDialog.h"
+#include "../export-import/CsvExportDialog.h"
+#include "../export-import/CsvImportDialog.h"
+#include "../settings/FontColorSettingsDialog.h"
+#include "../settings/StudySettingsDialog.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../study/WordDrillModel.h"
+#include "../study/SpacedRepetitionModel.h"
+#include "../study/WordDrillWindow.h"
+#include "../study/SpacedRepetitionWindow.h"
+#include "../study/CardSideView.h"
+#include "../utils/RandomGenerator.h"
+#include "../statistics/StatisticsView.h"
+
+MainWindow::MainWindow(AppModel* model):
+ workPath(""),
+ addImagePath(""),
+ model(model), welcomeScreen(NULL), studyWindow(NULL)
+{
+ init();
+ createCentralWidget();
+ createActions();
+ createMenus();
+ createToolBars();
+ createStatusBar();
+ FieldStyleFactory::inst()->load();
+ createDockWindows();
+ readSettings();
+ openSession();
+}
+
+MainWindow::~MainWindow()
+{
+ delete studyWindow;
+}
+
+void MainWindow::init()
+{
+ setWindowTitle(Strings::tr(Strings::s_appTitle));
+ setWindowIcon(QIcon(":/images/freshmemory.png"));
+}
+
+void MainWindow::activate()
+{
+ activateWindow();
+ raise();
+}
+
+void MainWindow::updateDictTab()
+{
+ int curIndex = dictTabWidget->currentIndex();
+ const Dictionary* curDict = model->curDictionary();
+ if(!curDict)
+ return;
+ if(!curDict->contentModified())
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/openbook-24.png") );
+ else
+ dictTabWidget->setTabIcon( curIndex, QIcon(":/images/filesave.png") );
+ dictTabWidget->setTabText(curIndex, curDict->shortName());
+ dictTabWidget->setTabToolTip(curIndex, QDir::toNativeSeparators( curDict->getFilePath()));
+}
+
+void MainWindow::updateTotalRecordsLabel()
+{
+ const Dictionary* curDict = model->curDictionary();
+ if(curDict)
+ {
+ totalRecordsLabel->show();
+ totalRecordsLabel->setText( tr("Records: %1").arg(curDict->entriesNum()) );
+ }
+ else
+ totalRecordsLabel->hide();
+}
+
+void MainWindow::updateActions()
+{
+ const Dictionary* curDict = model->curDictionary();
+ bool isCurDictModified = curDict && curDict->contentModified();
+
+ saveAct->setEnabled( isCurDictModified );
+ saveAsAct->setEnabled( curDict );
+ saveCopyAct->setEnabled( curDict );
+ exportAct->setEnabled( curDict );
+ removeTabAct->setEnabled( curDict );
+ updatePasteAction();
+ insertRecordsAct->setEnabled( curDict );
+ findAct->setEnabled( curDict );
+ if( findPanel )
+ findAgainAct->setEnabled( findPanel->canFindAgain() );
+ wordDrillAct->setEnabled(curDict);
+ spacedRepetitionAct->setEnabled(curDict);
+ statisticsAct->setEnabled(curDict);
+ dictionaryOptionsAct->setEnabled(curDict);
+}
+
+void MainWindow::updateAddImageAction()
+{
+ addImageAct->setEnabled(dictTabWidget->isInEditingState());
+}
+
+void MainWindow::updateSelectionActions()
+ {
+ if( !model->curDictionary() )
+ return;
+ const DictTableView* tableView = getCurDictView();
+ if( !tableView )
+ return;
+ Q_ASSERT( tableView->selectionModel() );
+ bool hasSelection = tableView->selectionModel()->hasSelection();
+ foreach( QAction* action, selectionActions )
+ action->setEnabled( hasSelection );
+ }
+
+void MainWindow::updatePasteAction()
+{
+ bool isCurTabValid = dictTabWidget->currentIndex() >= 0;
+ bool hasClipboardText = !QApplication::clipboard()->text().isEmpty();
+ pasteAct->setEnabled( isCurTabValid && hasClipboardText );
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+if( proposeToSave() )
+ {
+ writeSettings();
+ event->accept();
+ qApp->quit();
+ }
+else
+ event->ignore();
+}
+
+void MainWindow::newFile()
+ {
+ addDictTab(model->newDictionary());
+ updatePacksTreeView();
+ }
+
+void MainWindow::openFileWithDialog()
+ {
+ openFileWithDialog(workPath);
+ }
+
+void MainWindow::openOnlineDictionaries()
+ {
+ QDesktopServices::openUrl(QUrl("http://fresh-memory.com/dictionaries"));
+ }
+
+void MainWindow::openFileWithDialog(const QString& dirPath)
+ {
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Open dictionary"), dirPath,
+ tr("Dictionaries", "Filter name in dialog")+ " (*.fmd)");
+ if( filePath.isEmpty() )
+ return;
+ openFile(filePath);
+ }
+
+void MainWindow::openFile(const QString& filePath)
+ {
+ QString internalFilePath = QDir::fromNativeSeparators(filePath);
+ workPath = QFileInfo(internalFilePath).path();
+ int dictIndex = model->indexOfDictionary(internalFilePath);
+ if(isDictOpened(dictIndex))
+ setCurDictionary(dictIndex);
+ else
+ if(!loadFile(internalFilePath))
+ return;
+ updateAfterOpenedDictionary(internalFilePath);
+ }
+
+bool MainWindow::isDictOpened(int index)
+{
+ return index > -1;
+}
+
+void MainWindow::setCurDictionary(int index)
+{
+ if(index == -1)
+ return;
+ model->setCurDictionary(index);
+ setCurDictTab(index);
+}
+
+bool MainWindow::loadFile(const QString& filePath)
+{
+ bool ok = model->openDictionary(filePath);
+ if(ok)
+ addDictTab(model->curDictionary());
+ else
+ showOpenFileError();
+ return ok;
+}
+
+void MainWindow::showOpenFileError()
+{
+ QMessageBox::critical(this, Strings::errorTitle(), model->getErrorMessage());
+}
+
+void MainWindow::updateAfterOpenedDictionary(const QString& filePath)
+{
+ recentFilesMan->addFile(filePath);
+ updatePacksTreeView();
+ updateCardPreview();
+}
+
+void MainWindow::addDictTab(Dictionary* dict)
+ {
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(contentModifiedChanged(bool)), SLOT(updateActions()) );
+ connect( dict, SIGNAL(studyModifiedChanged(bool)), SLOT(saveStudyWithDelay(bool)) );
+ connect( dict, SIGNAL(filePathChanged()), SLOT(updateDictTab()) );
+ connect( dict, SIGNAL(entriesInserted(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(entriesRemoved(int,int)), SLOT(updateTotalRecordsLabel()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updatePacksTreeView()) );
+ connect( dict, SIGNAL(cardsGenerated()), SLOT(updateCardPreview()) );
+ connect( dict, SIGNAL(destroyed()), SLOT(updatePacksTreeView()) );
+
+ dictTabWidget->addDictTab(model->curDictModel());
+ DictTableView* tableView = const_cast<DictTableView*>( dictTabWidget->curDictView() );
+
+ tableView->addActions(contextMenuActions);
+ tableView->verticalHeader()->addActions(contextMenuActions);
+ tableView->setContextMenuPolicy( Qt::ActionsContextMenu );
+ tableView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu );
+
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateSelectionActions()) );
+ connect( tableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ SLOT(updateCardPreview()) );
+ connect( tableView, SIGNAL(destroyed(QAbstractItemModel*)), model, SLOT(removeDictModel(QAbstractItemModel*)) );
+ updateDictTab();
+ updateTotalRecordsLabel();
+ updateSelectionActions();
+ packsTreeView->reset();
+ QModelIndex firstIx = tableView->model()->index(0, 0, QModelIndex());
+ tableView->setCurrentIndex(firstIx);
+ tableView->setFocus();
+ }
+
+const DictTableView* MainWindow::getCurDictView() const
+ {
+ if(studyWindow)
+ {
+ const DictTableView* editCardView = studyWindow->cardEditView();
+ if( editCardView )
+ return editCardView;
+ }
+ return dictTabWidget->curDictView();
+ }
+
+/// Fork to SaveAs() or really save in DoSave()
+bool MainWindow::Save()
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() || dict->nameIsTemp() )
+ return SaveAs();
+else
+ return doSave( filePath );
+}
+
+bool MainWindow::SaveAs( bool aChangeFilePath )
+{
+Dictionary* dict = model->curDictionary();
+if( !dict )
+ return false;
+QString filePath = dict->getFilePath();
+if( filePath.isEmpty() )
+ filePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+filePath = QFileDialog::getSaveFileName(this, tr("Save dictionary as ..."), filePath);
+if( filePath.isEmpty() )
+ return false;
+workPath = QFileInfo( filePath ).path();
+if( doSave( filePath, aChangeFilePath ) )
+ {
+ recentFilesMan->addFile(filePath);
+ return true;
+ }
+else
+ return false;
+}
+
+void MainWindow::SaveCopy()
+{
+ SaveAs( false ); // Do not change the file name
+}
+
+void MainWindow::importFromCsv()
+{
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Import CSV file"), workPath);
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ CsvImportDialog importDialog( this, filePath, model );
+ if( importDialog.exec() )
+ {
+ model->addDictionary( importDialog.getDictionary() );
+ addDictTab(importDialog.getDictionary());
+ importDialog.getDictionary()->generateCards();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::exportToCsv()
+{
+ Dictionary* dict = model->curDictionary();
+ if( !dict )
+ return;
+ CsvExportDialog exportDialog( this, dict );
+ if( exportDialog.exec() )
+ {
+ QString dictFilePath( dict->getFilePath() );
+ if( dictFilePath.isEmpty() )
+ dictFilePath = workPath + "/" + Dictionary::tr( Dictionary::NoName ) + Dictionary::DictFileExtension;
+ QString filePath = QFileDialog::getSaveFileName( this, tr("Export to CSV file"), dictFilePath + ".txt" );
+ if( filePath.isEmpty() )
+ return;
+ workPath = QFileInfo( filePath ).path();
+ exportDialog.SaveCSVToFile( filePath );
+ }
+}
+
+bool MainWindow::doSave( const QString& aFilePath, bool aChangeFilePath )
+{
+ QFile::FileError error = model->curDictionary()->save( aFilePath, aChangeFilePath );
+ if( error != QFile::NoError )
+ {
+ QMessageBox::warning( this, Strings::errorTitle(), (tr("Cannot save dictionary:") + "\n%1").
+ arg(QDir::toNativeSeparators(aFilePath)) );
+ return false;
+ }
+ dictTabWidget->cleanUndoStack();
+ updateDictTab();
+ updateActions();
+ return true;
+}
+
+bool MainWindow::saveStudy()
+{
+ QTime saveTime;
+ saveTime.start();
+ Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return false;
+ if(dict->saveStudy() != QFile::NoError)
+ {
+ QMessageBox::warning(this, Strings::errorTitle(),
+ (tr("Cannot save study file:") + "\n%1").
+ arg(QDir::toNativeSeparators(
+ model->curDictionary()->getStudyFilePath())));
+ return false;
+ }
+ return true;
+}
+
+void MainWindow::saveStudyWithDelay(bool studyModified)
+{
+ if(studyModified)
+ QTimer::singleShot(AutoSaveStudyInterval, this, SLOT(saveStudy()));
+}
+
+/**
+ * Checks if all files are saved. If not, proposes to the user to save them.
+ * This function is called before closing the application.
+ * @return true, if all files are now saved and the application can be closed
+ */
+bool MainWindow::proposeToSave()
+{
+for(int i=0; i<dictTabWidget->count(); i++)
+ if( !proposeToSave(i) )
+ return false;
+return true;
+}
+
+/**
+ * Checks if the specified file is modified and if it is, proposes to the user to save it.
+ * If the specified file is not modified, the function just returns true.
+ *
+ * Function silently saves the study data, if it is modified.
+ *
+ * @param aTabIndex Index of the tab containing the modified file
+ * @return true, if the file is saved and the application can be closed.
+ * false, if the user declined the proposition to save the file.
+ */
+bool MainWindow::proposeToSave(int aTabIndex)
+{
+const Dictionary* dict = model->dictionary( aTabIndex );
+
+if( dict->contentModified() )
+ {
+ dictTabWidget->setCurrentIndex( aTabIndex );
+ QMessageBox::StandardButton pressedButton;
+ pressedButton = QMessageBox::warning( this, tr("Save dictionary?"), tr("Dictionary %1 was modified.\n"
+ "Save changes?").arg( dict->shortName(false) ),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel );
+ if( pressedButton == QMessageBox::Yes )
+ {
+ bool saved = Save();
+ if( !saved )
+ return false;
+ }
+ else if( pressedButton == QMessageBox::Cancel )
+ return false;
+ }
+if(dict->studyModified())
+ saveStudy();
+return true;
+}
+
+void MainWindow::openSession()
+{
+ QSettings settings;
+ QStringList sessionFiles = settings.value("session").toStringList();
+ if(sessionFiles.isEmpty())
+ {
+ QString lastFile = recentFilesMan->getLastUsedFilePath();
+ if(lastFile.isEmpty())
+ return;
+ sessionFiles << recentFilesMan->getLastUsedFilePath();
+ }
+ foreach(QString fileName, sessionFiles)
+ openFile(fileName);
+ int curTab = settings.value("session-cur-tab").toInt();
+ setCurDictionary(curTab);
+}
+
+void MainWindow::help()
+ {
+ QString fullVersion = QString(FM_VERSION);
+ QString version = fullVersion.left(fullVersion.lastIndexOf('.'));
+ QString url = QString("http://fresh-memory.com/docs/%1/index.html").arg(version);
+ QDesktopServices::openUrl(QUrl(url));
+ }
+
+void MainWindow::about()
+{
+ AboutDialog dialog(this);
+ dialog.exec();
+ dialog.setParent(NULL);
+}
+
+void MainWindow::addImage()
+ {
+ int cursorPos = getCurEditorCursorPos();
+ if(cursorPos < 0)
+ return;
+ const_cast<DictTableView*>(getCurDictView())->commitEditing();
+ QString selectedFile = selectAddImageFile();
+ if(selectedFile.isEmpty())
+ return;
+ selectedFile = copyImageFileToImagesDir(selectedFile);
+ if(selectedFile.isEmpty())
+ return;
+ insertImageIntoCurEditor(cursorPos, selectedFile);
+ }
+
+QString MainWindow::selectAddImageFile()
+ {
+ checkAddImagePath();
+ QString filePath = QFileDialog::getOpenFileName(this, tr("Add image"),
+ addImagePath,
+ tr("Images", "Filter name in dialog") +
+ " (*.png *.jpg *.jpeg *.gif *.svg *.xpm *.ico *.mng *.tiff);; " +
+ tr("All files") + " (*.*)");
+ if(filePath.isEmpty())
+ return QString();
+ addImagePath = QFileInfo(filePath).path();
+ return filePath;
+ }
+
+void MainWindow::checkAddImagePath()
+{
+ if(!addImagePath.isEmpty())
+ return;
+ if(!getCurDict())
+ return;
+ QString dicFilePath = getCurDict()->getFilePath();
+ addImagePath = QFileInfo(dicFilePath).path();
+}
+
+QString MainWindow::copyImageFileToImagesDir(const QString& filePath)
+{
+ QString dicImagesDir = getCurDict()->getImagesPath();
+ QString imageDir = QFileInfo(filePath).path();
+ if(imageDir == dicImagesDir)
+ return filePath;
+ QString newFilePath = createNewImageFilePath(dicImagesDir, filePath);
+ if(!QFileInfo(dicImagesDir).exists())
+ QDir(dicImagesDir).mkpath(".");
+ if(!QFile::copy(filePath, newFilePath))
+ return QString();
+ return newFilePath;
+}
+
+QString MainWindow::createNewImageFilePath(const QString& dicImagesDir,
+ const QString& filePath)
+{
+ int num = 0;
+ QString newFilePath;
+ do
+ {
+ newFilePath = createImagesDirFilePath(dicImagesDir, filePath, num);
+ num++;
+ }
+ while(QFileInfo(newFilePath).exists());
+ return newFilePath;
+}
+
+QString MainWindow::createImagesDirFilePath(const QString& dicImagesDir,
+ const QString& filePath, int suffixNum)
+{
+ QString imageFileName = QFileInfo(filePath).completeBaseName();
+ if(suffixNum > 0)
+ imageFileName += "-" + QString::number(suffixNum);
+ imageFileName += "." + QFileInfo(filePath).suffix();
+ return QDir(dicImagesDir).filePath(imageFileName);
+}
+
+int MainWindow::getCurEditorCursorPos()
+ {
+ const DictTableView* dictView = getCurDictView();
+ if(!dictView)
+ return -1;
+ return dictView->getEditorCursorPos();
+ }
+
+int MainWindow::getCurRow()
+{
+ return getCurDictView()->currentIndex().row();
+}
+
+int MainWindow::getCurColumn()
+{
+ return getCurDictView()->currentIndex().column();
+}
+
+Dictionary* MainWindow::getCurDict()
+{
+ return model->curDictionary();
+}
+
+void MainWindow::insertImageIntoCurEditor(int cursorPos, const QString& filePath)
+{
+ const DictTableView* dictView = getCurDictView();
+ const_cast<DictTableView*>(dictView)->startEditing(getCurRow(), getCurColumn());
+ dictView->insertImageIntoEditor(cursorPos, filePath);
+}
+
+void MainWindow::insertRecords()
+ {
+ pushToUnoStack(new InsertRecordsCmd(this));
+ }
+
+void MainWindow::removeRecords()
+ {
+ pushToUnoStack(new RemoveRecordsCmd(this));
+ }
+
+void MainWindow::pushToUnoStack(QUndoCommand* command)
+ {
+ getCurDictView()->dicTableModel()->undoStack()->push(command);
+ }
+
+void MainWindow::find()
+ {
+ findPanel->show();
+ findAgainAct->setEnabled(true);
+ }
+
+void MainWindow::findAgain()
+ {
+ findPanel->find();
+ }
+
+void MainWindow::createActions()
+{
+ createFileActions();
+ createEditActions();
+ createDictContextMenuActions();
+ createSelectionActions();
+ createToolsActions();
+ createSettingsActions();
+ createHelpActions();
+ initActions();
+}
+
+void MainWindow::createFileActions()
+{
+ newAct = new QAction(QIcon(":/images/filenew.png"), tr("&New"), this);
+ newAct->setShortcut(tr("Ctrl+N"));
+ connect(newAct, SIGNAL(triggered()), SLOT(newFile()));
+
+ loadAct = new QAction(QIcon(":/images/fileopen.png"), tr("&Open ..."), this);
+ loadAct->setShortcut(tr("Ctrl+O"));
+ connect(loadAct, SIGNAL(triggered()), SLOT(openFileWithDialog()));
+
+ openFlashcardsAct = new QAction(tr("Online dictionaries"), this);
+ connect(openFlashcardsAct, SIGNAL(triggered()), SLOT(openOnlineDictionaries()));
+
+ saveAct = new QAction(QIcon(":/images/filesave.png"), tr("&Save"), this);
+ saveAct->setShortcut(tr("Ctrl+S"));
+ connect(saveAct, SIGNAL(triggered()), SLOT(Save()));
+
+ saveAsAct = new QAction(QIcon(":/images/filesaveas.png"), tr("Save &as ..."), this);
+ connect(saveAsAct, SIGNAL(triggered()), SLOT(SaveAs()));
+
+ saveCopyAct = new QAction(tr("Save &copy ..."), this);
+ connect(saveCopyAct, SIGNAL(triggered()), SLOT(SaveCopy()));
+
+ importAct = new QAction(tr("&Import from CSV ..."), this);
+ connect(importAct, SIGNAL(triggered()), SLOT(importFromCsv()));
+
+ exportAct = new QAction(tr("&Export to CSV ..."), this);
+ connect(exportAct, SIGNAL(triggered()), SLOT(exportToCsv()));
+
+ removeTabAct = new QAction(QIcon(":/images/remove.png"),
+ tr("&Close dictionary"), this);
+ removeTabAct->setShortcut(tr("Ctrl+W"));
+ connect(removeTabAct, SIGNAL(triggered()), dictTabWidget, SLOT(closeTab()));
+
+ quitAct = new QAction(QIcon(":/images/exit.png"), tr("&Quit"), this);
+ quitAct->setShortcut(tr("Ctrl+Q"));
+ connect(quitAct, SIGNAL(triggered()), SLOT(close()));
+}
+
+void MainWindow::createEditActions()
+{
+ undoAct = dictTabWidget->undoGroup()->createUndoAction(this);
+ undoAct->setShortcut(tr("Ctrl+Z"));
+
+ redoAct = dictTabWidget->undoGroup()->createRedoAction(this);
+ redoAct->setShortcut(tr("Ctrl+Y"));
+
+ copyAct = new QAction(QIcon(":/images/editcopy.png"), tr("&Copy"), this);
+ copyAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+C") << QString("Ctrl+Insert"));
+ connect(copyAct, SIGNAL(triggered()), SLOT(copyEntries()));
+
+ cutAct = new QAction(QIcon(":/images/editcut.png"), tr("Cu&t"), this);
+ cutAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+X") << QString("Shift+Delete"));
+ connect(cutAct, SIGNAL(triggered()), SLOT(cutEntries()));
+
+ pasteAct = new QAction(QIcon(":/images/editpaste.png"), tr("&Paste"), this);
+ pasteAct->setShortcuts(QList<QKeySequence>() << tr("Ctrl+V") << QString("Shift+Insert"));
+ connect(pasteAct, SIGNAL(triggered()), SLOT(pasteEntries()));
+ connect(QApplication::clipboard(), SIGNAL(dataChanged()), SLOT(updatePasteAction()));
+
+ createAddImageAct();
+ createinsertRecordsAct();
+ createRemoveRecordsAct();
+
+ findAct = new QAction(QIcon(":/images/find.png"), tr("&Find..."), this);
+ findAct->setShortcut(tr("Ctrl+F"));
+ connect(findAct, SIGNAL(triggered()), SLOT(find()));
+
+ findAgainAct = new QAction(QIcon(":/images/next.png"), tr("Find &again"), this);
+ findAgainAct->setShortcut(QString("F3"));
+ connect(findAgainAct, SIGNAL(triggered()), SLOT(findAgain()));
+}
+
+void MainWindow::createAddImageAct()
+{
+ addImageAct = new QAction(QIcon(":/images/add-image.png"), tr("&Add image"), this);
+ addImageAct->setShortcut(tr("Ctrl+G"));
+ connect(addImageAct, SIGNAL(triggered()), SLOT(addImage()));
+ connect(dictTabWidget, SIGNAL(editingStateChanged()), SLOT(updateAddImageAction()));
+}
+
+void MainWindow::createinsertRecordsAct()
+{
+ insertRecordsAct = new QAction(QIcon(":/images/add.png"),
+ tr("&Insert record"), this);
+ insertRecordsAct->setShortcut(tr("Ctrl+I"));
+ connect(insertRecordsAct, SIGNAL(triggered()), SLOT(insertRecords()));
+}
+
+void MainWindow::createRemoveRecordsAct()
+{
+ removeRecordsAct = new QAction(QIcon(":/images/delete.png"),
+ tr("&Remove record"), this);
+ removeRecordsAct->setShortcut(Qt::Key_Delete);
+ connect(removeRecordsAct, SIGNAL(triggered()), SLOT(removeRecords()));
+}
+
+void MainWindow::createToolsActions()
+{
+ QSignalMapper* startStudySignalMapper = new QSignalMapper(this);
+ connect(startStudySignalMapper, SIGNAL(mapped(int)), SLOT(startStudy(int)));
+
+ wordDrillAct = new QAction(QIcon(":/images/word-drill.png"), tr("&Word drill"),
+ this);
+ wordDrillAct->setShortcut(QString("F5"));
+ connect(wordDrillAct, SIGNAL(triggered()), startStudySignalMapper, SLOT(map()));
+ startStudySignalMapper->setMapping(wordDrillAct, AppModel::WordDrill);
+
+ spacedRepetitionAct = new QAction(QIcon(":/images/spaced-rep.png"),
+ tr("&Spaced repetition"), this);
+ spacedRepetitionAct->setShortcut(QString("F6"));
+ connect(spacedRepetitionAct, SIGNAL(triggered()), startStudySignalMapper,
+ SLOT(map()));
+ startStudySignalMapper->setMapping(spacedRepetitionAct, AppModel::SpacedRepetition);
+
+ statisticsAct = new QAction(QIcon(":/images/statistics.png"), tr("S&tatistics"),
+ this);
+ statisticsAct->setShortcut(QString("F7"));
+ connect(statisticsAct, SIGNAL(triggered()), SLOT(showStatistics()));
+}
+
+void MainWindow::createSettingsActions()
+{
+ dictionaryOptionsAct = new QAction(QIcon(":/images/dic-options.png"),
+ tr("&Dictionary options"), this);
+ dictionaryOptionsAct->setShortcut(QString("Ctrl+1"));
+ connect(dictionaryOptionsAct, SIGNAL(triggered()), SLOT(openDictionaryOptions()));
+
+ fontColorSettingsAct = new QAction(QIcon(":/images/font-style.png"),
+ tr("&Font and color settings"), this);
+ fontColorSettingsAct->setShortcut(QString("Ctrl+2"));
+ connect(fontColorSettingsAct, SIGNAL(triggered()), SLOT(openFontColorSettings()));
+
+ studySettingsAct = new QAction(QIcon(":/images/study-settings.png"),
+ tr("&Study settings"), this);
+ studySettingsAct->setShortcut(QString("Ctrl+3"));
+ connect(studySettingsAct, SIGNAL(triggered()), SLOT(openStudySettings()));
+}
+
+void MainWindow::createHelpActions()
+{
+ helpAct = new QAction(tr("Help"), this);
+ helpAct->setShortcut(QString("F1"));
+ connect(helpAct, SIGNAL(triggered()), SLOT(help()));
+
+ aboutAct = new QAction(QIcon(":/images/freshmemory.png"), tr("About"), this);
+ connect(aboutAct, SIGNAL(triggered()), SLOT(about()));
+}
+
+void MainWindow::initActions()
+{
+ saveAct->setEnabled(false);
+ saveAsAct->setEnabled(false);
+ saveCopyAct->setEnabled(false);
+ exportAct->setEnabled(false);
+ removeTabAct->setEnabled(false);
+
+ cutAct->setEnabled(false);
+ copyAct->setEnabled(false);
+ pasteAct->setEnabled(false);
+ addImageAct->setEnabled(false);
+ insertRecordsAct->setEnabled(false);
+ removeRecordsAct->setEnabled(false);
+ findAct->setEnabled(false);
+ findAgainAct->setEnabled(false);
+
+ statisticsAct->setEnabled(false);
+ wordDrillAct->setEnabled(false);
+ spacedRepetitionAct->setEnabled(false);
+
+ dictionaryOptionsAct->setEnabled(false);
+}
+
+void MainWindow::createDictContextMenuActions()
+{
+ contextMenuActions << copyAct;
+ contextMenuActions << cutAct;
+ contextMenuActions << pasteAct;
+ contextMenuActions << insertRecordsAct;
+ contextMenuActions << removeRecordsAct;
+}
+
+void MainWindow::createSelectionActions()
+{
+ selectionActions << copyAct;
+ selectionActions << cutAct;
+ selectionActions << removeRecordsAct;
+}
+
+void MainWindow::createMenus()
+{
+ createFileMenu();
+
+ editMenu = menuBar()->addMenu( tr("&Edit") );
+ editMenu->addAction( undoAct );
+ editMenu->addAction( redoAct );
+ editMenu->addSeparator();
+
+ editMenu->addAction(cutAct);
+ editMenu->addAction(copyAct);
+ editMenu->addAction(pasteAct);
+ editMenu->addSeparator();
+
+ editMenu->addAction(addImageAct);
+ editMenu->addAction(insertRecordsAct);
+ editMenu->addAction(removeRecordsAct);
+ editMenu->addSeparator();
+ editMenu->addAction(findAct);
+ editMenu->addAction(findAgainAct);
+
+ viewMenu = menuBar()->addMenu( tr("&View") );
+
+ toolsMenu = menuBar()->addMenu( tr("&Tools") );
+ toolsMenu->addAction(wordDrillAct);
+ toolsMenu->addAction(spacedRepetitionAct);
+ toolsMenu->addAction(statisticsAct);
+
+ createOptionsMenu();
+
+ menuBar()->addSeparator();
+
+ helpMenu = menuBar()->addMenu( tr("&Help") );
+ helpMenu->addAction(helpAct);
+ helpMenu->addAction(aboutAct);
+}
+
+void MainWindow::createFileMenu()
+{
+ fileMenu = menuBar()->addMenu(tr("&File"));
+ fileMenu->addAction(newAct);
+ fileMenu->addAction(loadAct);
+ fileMenu->addAction(openFlashcardsAct);
+ fileMenu->addAction(saveAct);
+ fileMenu->addAction(saveAsAct);
+ fileMenu->addAction(saveCopyAct);
+ fileMenu->addAction(importAct);
+ fileMenu->addAction(exportAct);
+
+ createRecentFilesMenu();
+
+ fileMenu->addSeparator();
+ fileMenu->addAction(removeTabAct);
+ fileMenu->addAction(quitAct);
+}
+
+void MainWindow::createRecentFilesMenu()
+{
+ QMenu* recentFilesMenu = fileMenu->addMenu(tr("&Recent files"));
+ welcomeScreen->setRecentFilesMenu(recentFilesMenu);
+ recentFilesMan = new RecentFilesManager(this, recentFilesMenu);
+ connect(recentFilesMan, SIGNAL(recentFileTriggered(const QString&)),
+ SLOT(openFile(const QString&)));
+ connect(recentFilesMan, SIGNAL(addedFile()), welcomeScreen,
+ SLOT(updateRecentFilesButton()));
+}
+
+void MainWindow::createOptionsMenu()
+{
+ optionsMenu = menuBar()->addMenu(tr("&Options"));
+ optionsMenu->addAction( dictionaryOptionsAct );
+ optionsMenu->addAction( fontColorSettingsAct );
+ optionsMenu->addAction( studySettingsAct );
+
+ new LanguageMenu(optionsMenu);
+}
+
+void MainWindow::createToolBars()
+{
+ QToolBar* toolBar = addToolBar( tr("Main") );
+ toolBar->setObjectName("Main");
+ toolBar->addAction(newAct);
+ toolBar->addAction(loadAct);
+ toolBar->addAction(saveAct);
+ toolBar->addSeparator();
+ toolBar->addAction(findAct);
+ toolBar->addAction(wordDrillAct);
+ toolBar->addAction(spacedRepetitionAct);
+ toolBar->addAction(statisticsAct);
+ toolBar->addAction(addImageAct);
+}
+
+void MainWindow::createStatusBar()
+{
+ statusBar()->addPermanentWidget( totalRecordsLabel = new QLabel );
+ updateTotalRecordsLabel();
+}
+
+void MainWindow::createCentralWidget()
+ {
+ mainStackedWidget = new QStackedWidget;
+ welcomeScreen = new WelcomeScreen(this);
+ mainStackedWidget->addWidget(welcomeScreen);
+ mainStackedWidget->addWidget(createDictionaryView());
+ setCentralWidget(mainStackedWidget);
+ }
+
+QWidget* MainWindow::createDictionaryView()
+{
+ dictTabWidget = new DictionaryTabWidget(this);
+ connect(dictTabWidget, SIGNAL(currentChanged(int)), SLOT(curTabChanged(int)));
+ findPanel = new FindPanel(this);
+ findPanel->hide();
+
+ QVBoxLayout* dictionaryViewLt = new QVBoxLayout;
+ dictionaryViewLt->setContentsMargins(0, 0, 0, 0);
+ dictionaryViewLt->addWidget(dictTabWidget);
+ dictionaryViewLt->addWidget(findPanel);
+
+ QWidget* dictionaryView = new QWidget;
+ dictionaryView->setLayout(dictionaryViewLt);
+ return dictionaryView;
+}
+
+void MainWindow::curTabChanged(int tabIndex)
+{
+ updateMainStackedWidget(tabIndex);
+ model->setCurDictionary(tabIndex);
+ updateActions();
+ updateSelectionActions();
+ updateTotalRecordsLabel();
+ updatePackSelection(tabIndex);
+ updateCardPreview();
+}
+
+void MainWindow::updateMainStackedWidget(int tabIndex)
+{
+ if (tabIndex == -1)
+ mainStackedWidget->setCurrentIndex(0);
+ else
+ mainStackedWidget->setCurrentIndex(1);
+}
+
+void MainWindow::createDockWindows()
+ {
+ createPacksTreeDock();
+ createCardPreviewDock();
+ }
+
+void MainWindow::createPacksTreeDock()
+{
+ QDockWidget* packsDock = new QDockWidget(tr("Card packs"));
+ packsDock->setObjectName("Card-packs");
+ packsDock->setAllowedAreas(
+ Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+ packsTreeView = new QTreeView;
+ packsTreeView->setAllColumnsShowFocus(true);
+ packsTreeView->setRootIsDecorated(false);
+ packsTreeView->header()->setDefaultSectionSize(150);
+ PacksTreeModel* packsTreeModel = new PacksTreeModel(model);
+ packsTreeView->setModel(packsTreeModel);
+ packsDock->setWidget(packsTreeView);
+ addDockWidget(Qt::LeftDockWidgetArea, packsDock);
+ viewMenu->addAction(packsDock->toggleViewAction());
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(setCurDictTabByTreeIndex(const QModelIndex&)));
+ connect(packsTreeView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateCardPreview()));
+ connect(packsTreeView, SIGNAL(activated(const QModelIndex&)),
+ SLOT(startSpacedRepetitionStudy()));
+}
+
+void MainWindow::createCardPreviewDock()
+{
+ cardPreview = new CardPreview(this);
+ addDockWidget(Qt::LeftDockWidgetArea, cardPreview);
+ viewMenu->addAction(cardPreview->toggleViewAction());
+}
+
+void MainWindow::updatePacksTreeView()
+{
+ static_cast<PacksTreeModel*>( packsTreeView->model() )->updateData();
+ packsTreeView->expandAll();
+ packsTreeView->resizeColumnToContents( 1 );
+ packsTreeView->resizeColumnToContents( 2 );
+ updatePackSelection();
+}
+
+void MainWindow::updateCardPreview()
+{
+ CardPack* pack = getCurCardPack();
+ Card* card = getCurCardFromPack(pack);
+ cardPreview->setContent(pack, card);
+}
+
+CardPack* MainWindow::getCurCardPack() const
+{
+ const Dictionary* dict = model->curDictionary();
+ if(!dict)
+ return NULL;
+ int packId = packsTreeView->currentIndex().row();
+ return dict->cardPack(packId);
+}
+
+const DicRecord* MainWindow::getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const
+{
+ if(!dict || !dictView)
+ return NULL;
+ return dict->getRecord(dictView->currentIndex().row());
+}
+
+const DicRecord* MainWindow::getCurDictRecord() const
+{
+ return getCurDictRecord(model->curDictionary(), getCurDictView());
+}
+
+Card* MainWindow::getCurCardFromPack(CardPack* pack) const
+{
+ const DicRecord* record = getCurDictRecord();
+ if(!record)
+ return NULL;
+ return pack->getCard(record->getPreviewQuestionForPack(pack));
+}
+
+/// Proxy-view-aware copying
+void MainWindow::copyEntries()
+ {
+ const DictTableView* tableView = getCurDictView(); // May be proxy view
+ if( !tableView )
+ return;
+ QModelIndexList selectedIndexes = tableView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( tableView->model() );
+ if( proxyModel )
+ {
+ QModelIndexList srcIndexes;
+ foreach( QModelIndex index, selectedIndexes )
+ {
+ QModelIndex srcIndex = proxyModel->mapToSource( index );
+ srcIndexes << srcIndex;
+ }
+ selectedIndexes = srcIndexes;
+ }
+ Dictionary* dict = model->curDictionary();
+ QList<DicRecord*> entries;
+ foreach( QModelIndex index, selectedIndexes )
+ entries << const_cast<DicRecord*>(dict->getRecord( index.row() ));
+
+ CsvExportData params;
+ params.commentChar = '#';
+ params.fieldSeparators = "& ";
+ params.quoteAllFields = false;
+ params.textDelimiter = '"';
+ params.writeColumnNames = true;
+ DicCsvWriter csvWriter( entries );
+ QString copiedStr = csvWriter.toCsvString( params );
+ QClipboard *clipboard = QApplication::clipboard();
+ clipboard->setText( copiedStr );
+ }
+
+void MainWindow::cutEntries()
+ {
+ copyEntries();
+ removeRecords();
+ }
+
+void MainWindow::pasteEntries()
+ {
+ // Get text from clipboard
+ QClipboard *clipboard = QApplication::clipboard();
+ QString pastedStr = clipboard->text();
+ if( pastedStr.isEmpty() )
+ return;
+
+ const DictTableView* dictView = getCurDictView(); // May be proxy view
+ Q_ASSERT( dictView );
+ DictTableModel* dictModel = dictView->dicTableModel(); // Always original model
+ Q_ASSERT( dictModel );
+
+ // Parse records to be pasted
+ DicCsvReader csvReader;
+ CsvImportData params;
+ params.firstLineIsHeader = true;
+ params.commentChar = '#';
+ params.fieldSeparationMode = EFieldSeparatorExactString;
+ params.fieldSeparators = "& ";
+ params.fromLine = 1;
+ params.textDelimiter = '"';
+ params.colsToImport = 0; // All
+ QList<DicRecord*> records = csvReader.readEntries( pastedStr, params );
+ if( records.empty() )
+ return;
+ QStringList pastedFieldNames = csvReader.fieldNames();
+
+ // Check for new pasted fields. Ask user what to do with them.
+ QStringList dicFieldNames = model->curDictionary()->fieldNames();
+ QStringList newFieldNames;
+ foreach( QString name, pastedFieldNames )
+ if( !dicFieldNames.contains( name ) )
+ newFieldNames << name;
+ if( !newFieldNames.isEmpty() )
+ {
+ QMessageBox msgBox;
+ msgBox.setWindowTitle( windowTitle() );
+ bool existingFields = pastedFieldNames.size() - newFieldNames.size() > 0;
+ msgBox.setText( tr("The pasted records contain %n new field(s)", "", newFieldNames.size()) + ":\n" + newFieldNames.join(", ") + "." );
+ msgBox.setInformativeText(tr("Do you want to add new fields to this dictionary?"));
+ msgBox.setIcon( QMessageBox::Question );
+ QPushButton* addFieldsBtn = msgBox.addButton( tr("Add new fields"), QMessageBox::YesRole );
+ QPushButton* pasteExistingBtn = msgBox.addButton( tr("Paste only existing fields"), QMessageBox::NoRole );
+ pasteExistingBtn->setEnabled( existingFields );
+ msgBox.addButton( QMessageBox::Cancel );
+ msgBox.setDefaultButton( addFieldsBtn );
+ int res = msgBox.exec();
+ QAbstractButton* clickedBtn = msgBox.clickedButton();
+ if( clickedBtn == addFieldsBtn )
+ dictModel->addFields( newFieldNames );
+ else if( res == QMessageBox::Cancel ) // do not paste records
+ return;
+ // If paste only existing fields, don't make any changes here, just continue.
+ }
+
+ QUndoCommand* command = new PasteRecordsCmd( records, this ); // Takes ownership of records
+ dictModel->undoStack()->push( command );
+ }
+
+void MainWindow::readSettings()
+{
+ loadGeneralSettings();
+ StudySettings::inst()->load();
+}
+
+void MainWindow::loadGeneralSettings()
+{
+ QSettings settings;
+ move(settings.value("main-pos", QPoint(100, 100)).toPoint());
+ resize(settings.value("main-size", QSize(600, 500)).toSize());
+ if (settings.value("main-maximized", false).toBool())
+ showMaximized();
+
+ restoreState(settings.value("main-state").toByteArray(), 0);
+ QStringList recentFiles = settings.value("main-recent-files").toStringList();
+ recentFilesMan->createRecentFileActs(recentFiles);
+ workPath = settings.value("work-path",
+ QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).
+ toString();
+}
+
+void MainWindow::writeSettings()
+{
+ saveGeneralSettings();
+}
+
+void MainWindow::saveGeneralSettings()
+{
+ QSettings settings;
+ settings.setValue("main-maximized", isMaximized());
+ if(!isMaximized())
+ {
+ settings.setValue("main-pos", pos());
+ settings.setValue("main-size", size());
+ }
+ settings.setValue("main-state", saveState(0));
+ settings.setValue("main-recent-files", recentFilesMan->getFiles());
+ settings.setValue("work-path", workPath);
+ saveSession();
+}
+
+void MainWindow::saveSession()
+{
+ QSettings settings;
+ QStringList sessionFiles;
+ for(int i = 0; i < dictTabWidget->count(); i++)
+ {
+ const Dictionary* dict = model->dictionary(i);
+ sessionFiles << dict->getFilePath();
+ }
+ settings.setValue("session", sessionFiles);
+ settings.setValue("session-cur-tab", model->getCurDictIndex());
+}
+
+void MainWindow::startStudy(int studyType)
+ {
+ int packIx = packsTreeView->currentIndex().row();
+
+ if(studyWindow)
+ {
+ if((int)studyWindow->getStudyType() == studyType)
+ {
+ studyWindow->showNormal();
+ studyWindow->raise();
+ studyWindow->activateWindow();
+ return;
+ }
+ else
+ {
+ disconnect(studyWindow, SIGNAL(destroyed()), this, SLOT(activate()));
+ studyWindow->close();
+ }
+ }
+
+ IStudyModel* studyModel = model->createStudyModel(studyType, packIx);
+ if(!studyModel)
+ {
+ QMessageBox::critical(this, Strings::errorTitle(),
+ tr("The study cannot be started.", "First part of error message") + "\n" +
+ model->getErrorMessage());
+ return;
+ }
+
+ if( studyType == AppModel::SpacedRepetition )
+ connect(studyModel, SIGNAL(nextCardSelected()), SLOT(updatePacksTreeView()));
+
+ // Create study window
+ switch( studyType )
+ {
+ case AppModel::WordDrill:
+ studyWindow = new WordDrillWindow(dynamic_cast<WordDrillModel*>(studyModel), this );
+ break;
+ case AppModel::SpacedRepetition:
+ studyWindow = new SpacedRepetitionWindow(dynamic_cast<SpacedRepetitionModel*>(studyModel), this );
+ break;
+ }
+ connect(studyWindow, SIGNAL(destroyed()), SLOT(activate()));
+ studyWindow->show();
+ }
+
+void MainWindow::startSpacedRepetitionStudy()
+ {
+ startStudy( AppModel::SpacedRepetition );
+ }
+
+void MainWindow::goToDictionaryRecord(const Dictionary* aDictionary, int aRecordRow )
+ {
+ int dicIx = model->indexOfDictionary( const_cast<Dictionary*>( aDictionary ) );
+ if( dicIx < 0 )
+ return;
+ show();
+ raise();
+ activateWindow();
+ dictTabWidget->goToDictionaryRecord( dicIx, aRecordRow );
+ }
+
+void MainWindow::openDictionaryOptions()
+{
+ Dictionary* curDict = model->curDictionary();
+ if( !curDict )
+ return;
+ DictionaryOptionsDialog dicOptions( curDict, this );
+ int res = dicOptions.exec();
+ if (res == QDialog::Accepted)
+ {
+ curDict->setDictConfig( &dicOptions.m_dict );
+ getCurDictView()->dicTableModel()->resetData();
+ updatePacksTreeView();
+ }
+}
+
+void MainWindow::openFontColorSettings()
+{
+ FontColorSettingsDialog dialog( this );
+ int res = dialog.exec();
+ if (res == QDialog::Accepted)
+ {
+ *(FieldStyleFactory::inst()) = *dialog.styleFactory();
+ FieldStyleFactory::inst()->save();
+ }
+}
+
+void MainWindow::openStudySettings()
+{
+ StudySettingsDialog dialog(this);
+ if(dialog.exec() == QDialog::Accepted)
+ {
+ *(StudySettings::inst()) = dialog.getSettings();
+ StudySettings::inst()->save();
+ }
+}
+
+void MainWindow::setCurDictTab(int index)
+{
+ dictTabWidget->setCurrentIndex(index);
+}
+
+void MainWindow::setCurDictTabByTreeIndex( const QModelIndex& aIndex )
+{
+ if( !aIndex.isValid() )
+ return;
+ dictTabWidget->setCurrentIndex( static_cast<TreeItem*>( aIndex.internalPointer() )->topParentRow() );
+}
+
+// Set the pack selection to the correct dictionary
+void MainWindow::updatePackSelection( int aDic )
+{
+ if( !model->curDictionary() )
+ return;
+ if( aDic == -1 ) // default value
+ aDic = model->curDictionary()->row();
+ QModelIndex curIx = packsTreeView->currentIndex();
+ if( curIx.isValid() && static_cast<TreeItem*>( curIx.internalPointer() )->topParentRow() == aDic )
+ return;
+ QAbstractItemModel* packsTreeModel = packsTreeView->model();
+ QModelIndex dicIx = packsTreeModel->index( aDic, 0, QModelIndex() );
+ int curPackIx;
+ if( !studyWindow.isNull() )
+ curPackIx = model->curCardPackIx();
+ else
+ curPackIx = 0; // first pack
+ QModelIndex packIndex = packsTreeModel->index( curPackIx, 0, dicIx );
+ packsTreeView->selectionModel()->setCurrentIndex( packIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows );
+}
+
+void MainWindow::showStatistics()
+{
+ StatisticsView(getCurDict()).exec();
+}
diff --git a/src/main-view/MainWindow.h b/src/main-view/MainWindow.h
new file mode 100644
index 0000000..5165118
--- /dev/null
+++ b/src/main-view/MainWindow.h
@@ -0,0 +1,218 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QtWidgets>
+
+class AppModel;
+class DictTableView;
+class Dictionary;
+class IStudyWindow;
+class FindPanel;
+class CardPreview;
+class RecentFilesManager;
+class DicRecord;
+class Card;
+class CardPack;
+class WelcomeScreen;
+
+#include "UndoCommands.h"
+#include "DictionaryTabWidget.h"
+
+class MainWindow: public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(AppModel* model);
+ ~MainWindow();
+
+ const DictTableView* getCurDictView() const;
+ QList<QAction*> getContextMenuActions() const { return contextMenuActions; }
+
+ void showContinueSearch() { dictTabWidget->showContinueSearch(); }
+ void goToDictionaryRecord( const Dictionary* aDictionary, int aRecordRow );
+
+public slots:
+ void openFile(const QString& filePath);
+
+private:
+ void init();
+ void addDictTab(Dictionary* dict);
+
+ void createActions();
+ void createFileActions();
+ void createEditActions();
+ void createAddImageAct();
+ void createinsertRecordsAct();
+ void createInsertRecordsAfterAct();
+ void createRemoveRecordsAct();
+ void createDictContextMenuActions();
+ void createSelectionActions();
+ void createToolsActions();
+ void createSettingsActions();
+ void createHelpActions();
+ void initActions();
+
+ void createMenus();
+ void createToolBars();
+ void createStatusBar();
+
+ void createCentralWidget();
+ void updateMainStackedWidget(int tabIndex);
+ QWidget* createDictionaryView();
+
+ void createDockWindows();
+ void createPacksTreeDock();
+ void createCardPreviewDock();
+
+ void readSettings();
+ void writeSettings();
+ bool doSave( const QString& aFilePath, bool aChangeFilePath = true );
+ bool proposeToSave();
+ void openSession();
+ void closeEvent(QCloseEvent *event);
+ void saveGeneralSettings();
+ void saveSession();
+ void loadGeneralSettings();
+
+ void updatePackSelection( int aDic = -1 );
+ Dictionary* getCurDict();
+ QString selectAddImageFile();
+ void checkAddImagePath();
+ QString copyImageFileToImagesDir(const QString& filePath);
+ QString createNewImageFilePath(const QString& dicImagesDir, const QString& filePath);
+ QString createImagesDirFilePath(const QString& dicImagesDir, const QString& filePath,
+ int suffixNum);
+ int getCurEditorCursorPos();
+ int getCurRow();
+ int getCurColumn();
+ void insertImageIntoCurEditor(int cursorPos, const QString& filePath);
+
+ void createFileMenu();
+ void createOptionsMenu();
+ void createRecentFilesMenu();
+
+ CardPack* getCurCardPack() const;
+ const DicRecord* getCurDictRecord() const;
+ const DicRecord* getCurDictRecord(const Dictionary* dict,
+ const QAbstractItemView* dictView) const;
+ Card* getCurCardFromPack(CardPack* pack) const;
+ bool isDictOpened(int index);
+ void setCurDictionary(int index);
+ void openFileWithDialog(const QString& dirPath);
+ bool loadFile(const QString& filePath);
+ void showOpenFileError();
+ void updateAfterOpenedDictionary(const QString& filePath);
+
+public slots:
+ void updateDictTab();
+ void updateActions();
+ void updatePasteAction();
+ void updateSelectionActions();
+ bool proposeToSave(int i);
+
+private slots:
+ void activate();
+ void curTabChanged(int tabIndex);
+ void updatePacksTreeView();
+ void updateCardPreview();
+ void updateAddImageAction();
+ void newFile();
+ void openFileWithDialog();
+ void openOnlineDictionaries();
+ bool Save();
+ bool saveStudy();
+ bool SaveAs( bool aChangeFilePath = true );
+ void SaveCopy();
+ void importFromCsv();
+ void exportToCsv();
+
+ void copyEntries();
+ void cutEntries();
+ void pasteEntries();
+ void addImage();
+ void insertRecords();
+ void removeRecords();
+ void pushToUnoStack(QUndoCommand* command);
+ void find();
+ void findAgain();
+ void openDictionaryOptions();
+ void openFontColorSettings();
+ void openStudySettings();
+
+ void startStudy(int aStudyType);
+ void startSpacedRepetitionStudy();
+ void help();
+ void about();
+ void updateTotalRecordsLabel();
+ void setCurDictTab(int index);
+ void setCurDictTabByTreeIndex(const QModelIndex& aIndex);
+ void showStatistics();
+
+ void saveStudyWithDelay(bool studyModified);
+
+private:
+ static const int AutoSaveStudyInterval = 3 * 60 * 1000; // ms
+
+private:
+ QString workPath;
+ QString addImagePath;
+ AppModel* model; // not own
+
+ // UI elements
+ QStackedWidget* mainStackedWidget;
+ DictionaryTabWidget* dictTabWidget;
+ WelcomeScreen* welcomeScreen;
+ QLabel* totalRecordsLabel;
+ QPointer<IStudyWindow> studyWindow;
+ FindPanel* findPanel;
+ QTreeView* packsTreeView;
+ CardPreview* cardPreview;
+
+ QMenu* fileMenu;
+ QMenu* editMenu;
+ QMenu* viewMenu;
+ QMenu* toolsMenu;
+ QMenu* optionsMenu;
+ QMenu* helpMenu;
+
+ RecentFilesManager* recentFilesMan;
+
+ // Actions
+ QAction* newAct;
+ QAction* loadAct;
+ QAction* openFlashcardsAct;
+ QAction* saveAct;
+ QAction* saveAsAct;
+ QAction* saveCopyAct;
+ QAction* importAct;
+ QAction* exportAct;
+ QAction* removeTabAct;
+ QAction* quitAct;
+
+ QAction* undoAct;
+ QAction* redoAct;
+ QAction* cutAct;
+ QAction* copyAct;
+ QAction* pasteAct;
+ QAction* addImageAct;
+ QAction* insertRecordsAct;
+ QAction* removeRecordsAct;
+ QAction* findAgainAct;
+ QAction* findAct;
+ QList<QAction*> contextMenuActions;
+ QList<QAction*> selectionActions;
+
+ QAction* wordDrillAct;
+ QAction* spacedRepetitionAct;
+ QAction* statisticsAct;
+
+ QAction* dictionaryOptionsAct;
+ QAction* fontColorSettingsAct;
+ QAction* studySettingsAct;
+
+ QAction* helpAct;
+ QAction* aboutAct;
+};
+
+#endif
diff --git a/src/main-view/PacksTreeModel.cpp b/src/main-view/PacksTreeModel.cpp
new file mode 100644
index 0000000..55b8c34
--- /dev/null
+++ b/src/main-view/PacksTreeModel.cpp
@@ -0,0 +1,93 @@
+#include "PacksTreeModel.h"
+#include "AppModel.h"
+#include "../dictionary/TreeItem.h"
+
+PacksTreeModel::PacksTreeModel( AppModel* aAppModel, QObject* aParent ):
+ QAbstractItemModel( aParent ), m_appModel( aAppModel )
+ {
+ }
+
+QVariant PacksTreeModel::data(const QModelIndex &index, int role) const
+ {
+ if( !index.isValid() )
+ return QVariant();
+ if( role != Qt::DisplayRole )
+ return QVariant();
+ TreeItem* item = static_cast<TreeItem*>( index.internalPointer() );
+ return item->data( index.column() );
+ }
+
+Qt::ItemFlags PacksTreeModel::flags(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return 0;
+ if( !index.parent().isValid() ) // First level item = dictionary
+ return Qt::ItemIsEnabled;
+ else // pack
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ }
+
+QVariant PacksTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
+ {
+ if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
+ switch( section )
+ {
+ case 0: return tr("Card pack"); break;
+ case 1: return tr("Sched"); break;
+ case 2: return tr("New"); break;
+ }
+ return QVariant();
+ }
+
+QModelIndex PacksTreeModel::index(int row, int column, const QModelIndex &parent) const
+ {
+ if( !hasIndex(row, column, parent) )
+ return QModelIndex();
+ if( !parent.isValid() ) // First level item = dictionary
+ {
+ Dictionary* dic = m_appModel->dictionary( row );
+ if( dic )
+ return createIndex( row, column, dic );
+ else
+ return QModelIndex();
+ }
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ const TreeItem* childItem = parentItem->child( row );
+ if( childItem )
+ return createIndex( row, column, (void*)childItem );
+ else
+ return QModelIndex();
+ }
+
+QModelIndex PacksTreeModel::parent(const QModelIndex &index) const
+ {
+ if( !index.isValid() )
+ return QModelIndex();
+ TreeItem* childItem = static_cast<TreeItem*>( index.internalPointer() );
+ if( !childItem )
+ return QModelIndex();
+ const TreeItem* parentItem = childItem->parent();
+
+ if( parentItem )
+ return createIndex( parentItem->row(), 0, (void*)parentItem );
+ else
+ return QModelIndex(); // The root item
+ }
+
+int PacksTreeModel::rowCount(const QModelIndex &parent) const
+ {
+ if( parent.column() > 0 ) // Only the first column may have children
+ return 0;
+ if( !parent.isValid() ) // Root item
+ return m_appModel->dictionariesNum();
+ TreeItem* parentItem = static_cast<TreeItem*>( parent.internalPointer() );
+ return parentItem->childCount();
+ }
+
+void PacksTreeModel::updateData() // TODO: Suspicious method, just reveals protected methods
+ {
+ beginResetModel();
+ endResetModel();
+ }
+
+
diff --git a/src/main-view/PacksTreeModel.h b/src/main-view/PacksTreeModel.h
new file mode 100644
index 0000000..f04036d
--- /dev/null
+++ b/src/main-view/PacksTreeModel.h
@@ -0,0 +1,32 @@
+#ifndef PACKSTREEMODEL_H
+#define PACKSTREEMODEL_H
+
+#include <QAbstractItemModel>
+
+#include "../dictionary/CardPack.h"
+
+class AppModel;
+
+class PacksTreeModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+public:
+ PacksTreeModel( AppModel* aAppModel, QObject* aParent = 0);
+
+ QVariant data( const QModelIndex &index, int role ) const;
+ Qt::ItemFlags flags( const QModelIndex &index ) const;
+ QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
+ QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const;
+ QModelIndex parent( const QModelIndex &index ) const;
+ int rowCount( const QModelIndex &parent = QModelIndex() ) const;
+ int columnCount( const QModelIndex &/*parent*/ = QModelIndex() ) const { return 3; }
+
+public slots:
+ void updateData();
+
+private:
+ AppModel* m_appModel;
+};
+
+#endif // PACKSTREEMODEL_H
diff --git a/src/main-view/RecentFilesManager.cpp b/src/main-view/RecentFilesManager.cpp
new file mode 100644
index 0000000..d554eb4
--- /dev/null
+++ b/src/main-view/RecentFilesManager.cpp
@@ -0,0 +1,99 @@
+#include "RecentFilesManager.h"
+
+RecentFilesManager::RecentFilesManager(QObject* parent, QMenu* recentFilesMenu):
+ QObject(parent), recentFilesMenu(recentFilesMenu)
+{
+}
+
+void RecentFilesManager::createRecentFileActs(const QStringList& recentFiles)
+{
+ if(recentFiles.isEmpty())
+ {
+ recentFilesMenu->setEnabled(false);
+ return;
+ }
+ int recentFilesNum = recentFiles.size();
+ if(recentFilesNum > MaxRecentFiles)
+ recentFilesNum = MaxRecentFiles;
+ for(int i = 0; i < recentFilesNum; i++)
+ recentFilesMenu->addAction(createRecentFileAction(recentFiles[i]));
+ updateActionTexts();
+}
+
+QAction* RecentFilesManager::createRecentFileAction(const QString& filePath)
+{
+ QAction* action = new QAction(this);
+ action->setData(QDir::fromNativeSeparators(filePath));
+ connect(action, SIGNAL(triggered()), this, SLOT(triggerRecentFile()));
+ return action;
+}
+
+void RecentFilesManager::updateActionTexts()
+{
+ for(int i = 0; i < recentFilesMenu->actions().size(); i++)
+ {
+ QAction* action = recentFilesMenu->actions()[i];
+ QString filePath = getActionFile(action);
+ QString fileName = getShortFileName(filePath);
+ QString fileDir = QDir::toNativeSeparators(getShortDirPath(filePath));
+ QString text = QString("&%1 %2 (%3)").arg((i + 1) % 10)
+ .arg(fileName).arg(fileDir);
+ action->setText(text);
+ }
+}
+
+QString RecentFilesManager::getActionFile(QAction* action)
+{
+ return action->data().toString();
+}
+
+QString RecentFilesManager::getLastUsedFilePath() const
+{
+ if(recentFilesMenu->actions().isEmpty())
+ return QString();
+ return getActionFile(recentFilesMenu->actions().first());
+}
+
+QString RecentFilesManager::getShortDirPath(const QString& filePath) const
+{
+ QString path = QFileInfo(filePath).absolutePath();
+ QFontMetrics metrics(recentFilesMenu->font());
+ return metrics.elidedText(path, Qt::ElideMiddle, MaxPathLength);
+}
+
+void RecentFilesManager::triggerRecentFile()
+{
+ QAction *action = qobject_cast<QAction*>(sender());
+ if(action)
+ emit recentFileTriggered(getActionFile(action));
+}
+
+void RecentFilesManager::addFile( const QString& filePath)
+{
+ addRecentFileAction(filePath);
+ updateActionTexts();
+ emit addedFile();
+}
+
+void RecentFilesManager::addRecentFileAction(const QString& filePath)
+{
+ recentFilesMenu->setEnabled(true);
+ foreach(QAction* action, recentFilesMenu->actions())
+ if(action->data() == filePath)
+ recentFilesMenu->removeAction(action);
+ QAction* newAction = createRecentFileAction(filePath);
+ if(!recentFilesMenu->actions().isEmpty())
+ recentFilesMenu->insertAction(recentFilesMenu->actions().first(), newAction);
+ else
+ recentFilesMenu->addAction(newAction);
+ if(recentFilesMenu->actions().size() > MaxRecentFiles)
+ recentFilesMenu->removeAction(recentFilesMenu->actions().last());
+}
+
+QStringList RecentFilesManager::getFiles() const
+{
+ QStringList list;
+ foreach(QAction* action, recentFilesMenu->actions())
+ list << getActionFile(action);
+ return list;
+}
diff --git a/src/main-view/RecentFilesManager.h b/src/main-view/RecentFilesManager.h
new file mode 100644
index 0000000..7988a0e
--- /dev/null
+++ b/src/main-view/RecentFilesManager.h
@@ -0,0 +1,43 @@
+#ifndef RECENT_FILES_MANAGER_H
+#define RECENT_FILES_MANAGER_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class RecentFilesManager: public QObject
+{
+ Q_OBJECT
+
+public:
+ RecentFilesManager(QObject* parent, QMenu* recentFilesMenu);
+ void createRecentFileActs(const QStringList& recentFiles);
+ void addFile( const QString& filePath);
+ QString getLastUsedFilePath() const;
+ QStringList getFiles() const;
+
+private:
+ static QString getShortFileName(const QString& filePath)
+ { return QFileInfo(filePath).fileName(); }
+ static QString getActionFile(QAction* action);
+
+private:
+ QAction* createRecentFileAction(const QString& filePath);
+ void updateActionTexts();
+ QString getShortDirPath(const QString& filePath) const;
+ void addRecentFileAction(const QString& filePath);
+
+private slots:
+ void triggerRecentFile();
+
+signals:
+ void recentFileTriggered(const QString& filePath);
+ void addedFile();
+
+private:
+ static const int MaxRecentFiles = 10;
+ static const int MaxPathLength = 300;
+
+private:
+ QMenu* recentFilesMenu;
+};
+#endif
diff --git a/src/main-view/RecordEditor.cpp b/src/main-view/RecordEditor.cpp
new file mode 100644
index 0000000..3f7a96c
--- /dev/null
+++ b/src/main-view/RecordEditor.cpp
@@ -0,0 +1,122 @@
+#include "RecordEditor.h"
+
+#include <QtDebug>
+
+const float RecordEditor::EditorWidthIncrease = 1.5;
+
+RecordEditor::RecordEditor(QWidget* parent, const QRect& cellRect) :
+ QTextEdit(parent), cellRect(cellRect),
+ enabledSizeUpdates(true)
+{
+ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ connect( this, SIGNAL(textChanged()), SLOT(updateEditor()) );
+}
+
+void RecordEditor::startDrawing()
+{
+ clear();
+ enabledSizeUpdates = false;
+}
+
+void RecordEditor::endDrawing()
+{
+ enabledSizeUpdates = true;
+ updateEditor();
+}
+
+void RecordEditor::drawText(const QString& text)
+{
+ textCursor().insertText(text);
+}
+
+void RecordEditor::drawImage(const QString& filePath)
+{
+ QTextImageFormat format;
+ format.setVerticalAlignment(QTextCharFormat::AlignBaseline);
+ format.setName(filePath);
+ format.setHeight(ThumbnailSize);
+ textCursor().insertImage(format);
+}
+
+void RecordEditor::insertImage(int cursorPos, const QString& filePath)
+{
+ moveCursor(cursorPos);
+ drawImage(filePath);
+}
+
+void RecordEditor::moveCursor(int cursorPos)
+{
+ QTextCursor cursor = textCursor();
+ cursor.setPosition(cursorPos);
+ setTextCursor(cursor);
+}
+
+QString RecordEditor::getText() const
+{
+ QString text;
+ QTextBlock block = document()->begin();
+ while(block.isValid())
+ {
+ for(QTextBlock::iterator it = block.begin(); !it.atEnd(); it++)
+ {
+ QTextCharFormat format = it.fragment().charFormat();
+ if(format.isImageFormat())
+ text += QString("<img src=\"") + format.toImageFormat().name() + "\">";
+ else
+ text += it.fragment().text();
+ }
+ block = block.next();
+ }
+ return text;
+}
+
+QSize RecordEditor::sizeHint() const
+{
+ int width = getEditorWidth();
+ return QSize(width, getEditorHeight());
+}
+
+int RecordEditor::getEditorWidth() const
+{
+ int horMargins = contentsMargins().left() * 2;
+ int textWidth = cellRect.width() - horMargins;
+ document()->setTextWidth(textWidth);
+ if(textWrapped() && textWidth < MinWidth)
+ {
+ textWidth = MinWidth;
+ document()->setTextWidth(textWidth);
+ }
+ return textWidth + horMargins;
+}
+
+bool RecordEditor::textWrapped() const
+{
+ return document()->size().height() > cellRect.height();
+}
+
+int RecordEditor::getEditorHeight() const
+{
+ return document()->size().height() + contentsMargins().top() * 2;
+}
+
+void RecordEditor::updateEditor()
+{
+ if(!enabledSizeUpdates)
+ return;
+ adjustSize();
+ updatePos();
+}
+
+void RecordEditor::updatePos()
+{
+ QRect editorRect = cellRect;
+ editorRect.setSize(size());
+ QSize viewportSize = qobject_cast<QWidget*>(parent())->size();
+ if(editorRect.right() > viewportSize.width())
+ editorRect.moveRight(viewportSize.width());
+ if(editorRect.bottom() > viewportSize.height())
+ editorRect.moveBottom(viewportSize.height());
+ move(editorRect.topLeft());
+}
diff --git a/src/main-view/RecordEditor.h b/src/main-view/RecordEditor.h
new file mode 100644
index 0000000..40b5dc5
--- /dev/null
+++ b/src/main-view/RecordEditor.h
@@ -0,0 +1,46 @@
+#ifndef RECORD_EDITOR_H
+#define RECORD_EDITOR_H
+
+#include <QtWidgets>
+
+#include "FieldContentPainter.h"
+
+class RecordEditor: public QTextEdit, public FieldContentPainter
+{
+Q_OBJECT
+
+private:
+ static const int ThumbnailSize = 25;
+ static const int MinWidth = 150;
+ static const int MaxWidth = 400;
+ static const float EditorWidthIncrease;
+
+public:
+ RecordEditor(QWidget* parent, const QRect& cellRect);
+ QRect getCellRect() const { return cellRect; }
+ void startDrawing();
+ void endDrawing();
+ void drawText(const QString& text);
+ void drawImage(const QString& filePath);
+ void insertImage(int cursorPos, const QString& filePath);
+ QString getText() const;
+
+protected:
+ QSize sizeHint() const;
+
+private:
+ int getEditorWidth() const;
+ int getEditorHeight() const;
+ void updatePos();
+ bool textWrapped() const;
+ void moveCursor(int cursorPos);
+
+public slots:
+ void updateEditor();
+
+private:
+ QRect cellRect;
+ bool enabledSizeUpdates;
+};
+#endif
+
diff --git a/src/main-view/UndoCommands.cpp b/src/main-view/UndoCommands.cpp
new file mode 100644
index 0000000..349b2a7
--- /dev/null
+++ b/src/main-view/UndoCommands.cpp
@@ -0,0 +1,322 @@
+#include "UndoCommands.h"
+#include "DictTableModel.h"
+#include "DictTableView.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/DicRecord.h"
+#include "../main-view/MainWindow.h"
+#include "../main-view/CardFilterModel.h"
+
+UndoRecordCmd::UndoRecordCmd( const MainWindow* aMainWin ):
+ m_mainWindow( aMainWin )
+ {
+ }
+
+const DictTableView* UndoRecordCmd::getCurView()
+ {
+ if( !m_mainWindow )
+ return NULL;
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ return dictView;
+ }
+
+CardFilterModel* UndoRecordCmd::getProxyModel( const DictTableView* aCurDictView )
+ {
+ CardFilterModel* proxyModel = qobject_cast<CardFilterModel*>( aCurDictView->model() );
+ return proxyModel;
+ }
+
+void UndoRecordCmd::insertRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = getCurView();
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+
+ // Insert rows by positions in the direct order
+ foreach( int row, aRowNumbers )
+ {
+ if( proxyModel )
+ proxyModel->addFilterRow( row );
+ m_dictModel->insertRows( row, 1 );
+ }
+ }
+
+void UndoRecordCmd::setRecords( QMap<int, DicRecord*> aRecords )
+ {
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Init rows by positions in the direct order
+ foreach( int row, aRecords.keys() )
+ {
+ QModelIndex index = m_dictModel->index( row, 0 );
+ DicRecord* record = aRecords.value( row );
+ m_dictModel->setData( index, QVariant::fromValue( *record ), DictTableModel::DicRecordRole );
+ }
+ dict->enableRecordUpdates();
+ }
+
+void UndoRecordCmd::removeRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ Dictionary* dict = const_cast<Dictionary*>( m_dictModel->dictionary() );
+ dict->disableRecordUpdates();
+
+ // Remove records by ranges from back
+ QListIterator<int> i( aRowNumbers );
+ i.toBack();
+ while( i.hasPrevious() )
+ {
+ int pos = i.previous();
+ if( proxyModel )
+ proxyModel->removeFilterRow( pos );
+ m_dictModel->removeRows( pos, 1 );
+ }
+ dict->enableRecordUpdates();
+ }
+
+
+/** Selects rows in the list.
+ * Sets the first row in the list as current.
+ */
+void UndoRecordCmd::selectRows( QList<int> aRowNumbers )
+ {
+ const DictTableView* dictView = m_mainWindow->getCurDictView(); // May be proxy view
+ CardFilterModel* proxyModel = getProxyModel( dictView );
+ int topRow = aRowNumbers.first();
+ if( topRow > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ topRow = m_dictModel->rowCount() - 1;
+ QModelIndex topIndex = m_dictModel->index( topRow, 0, QModelIndex() );
+ QItemSelectionModel* selectionModel = dictView->selectionModel();
+ selectionModel->clear();
+ foreach( int row, aRowNumbers )
+ {
+ if( row > m_dictModel->rowCount() - 1 ) // don't go beyond table boundaries
+ row = m_dictModel->rowCount() - 1;
+ QModelIndex index = m_dictModel->index( row, 0, QModelIndex() );
+ if( proxyModel )
+ {
+ index = proxyModel->mapFromSource( index );
+ if( !index.isValid() )
+ continue;
+ }
+ selectionModel->setCurrentIndex( index, QItemSelectionModel::Select | QItemSelectionModel::Rows );
+ }
+ if( proxyModel )
+ topIndex = proxyModel->mapFromSource( topIndex );
+ if( topIndex.isValid() )
+ selectionModel->setCurrentIndex( topIndex, QItemSelectionModel::Current );
+ }
+
+void UndoRecordCmd::selectRow( int aRow )
+ {
+ QList<int> list;
+ list << aRow;
+ selectRows( list );
+ }
+
+//==============================================================================================================
+
+InsertRecordsCmd::InsertRecordsCmd( const MainWindow* aMainWin):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // If no selection, insert 1 row in the end
+ int pos = m_dictModel->rowCount();
+ int rowsNum = 1;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ if( !selectedRows.isEmpty() )
+ // Insert number of rows equal to the selected rows
+ {
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ qSort( selectedRows ); // to find the top row
+ pos = selectedRows.first().row();
+ rowsNum = selectedRows.size();
+ }
+ for( int r = pos; r < pos + rowsNum; r++ )
+ m_rowNumbers << r;
+
+ updateText();
+ }
+
+void InsertRecordsCmd::updateText()
+ {
+ setText(QObject::tr("Insert %n record(s)", "Undo action of inserting records", m_rowNumbers.size() ));
+ }
+
+void InsertRecordsCmd::redo()
+ {
+ insertRows( m_rowNumbers );
+ selectRows( m_rowNumbers );
+ }
+
+void InsertRecordsCmd::undo()
+ {
+ removeRows( m_rowNumbers );
+ selectRow( m_rowNumbers.first() );
+ }
+
+bool InsertRecordsCmd::mergeWith( const QUndoCommand* command )
+ {
+ const InsertRecordsCmd* otherInsertCmd = static_cast<const InsertRecordsCmd*>( command );
+ if( !otherInsertCmd )
+ return false;
+ if( otherInsertCmd->m_dictModel != m_dictModel )
+ return false;
+
+ // Find where to insert the other row numbers
+ int otherFirstRow = otherInsertCmd->m_rowNumbers.first();
+ int otherRowsNum = otherInsertCmd->m_rowNumbers.size();
+ int i = 0;
+ for( i = 0; i < m_rowNumbers.size(); i++ )
+ if( otherFirstRow <= m_rowNumbers[i] )
+ break;
+ int insertPos = i;
+ // Increment this row numbers by the number of other rows, after the insertion point
+ for( int i = insertPos; i < m_rowNumbers.size(); i++ )
+ m_rowNumbers[i] += otherRowsNum;
+ // Do the insertion of other rows
+ for( int i = 0; i < otherRowsNum; i++ )
+ m_rowNumbers.insert( insertPos + i, otherInsertCmd->m_rowNumbers[i] );
+
+ updateText();
+ return true;
+ }
+
+//==============================================================================================================
+
+RemoveRecordsCmd::RemoveRecordsCmd( const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ int recordsNum = 0;
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ { // convert selection to source indexes
+ QModelIndexList list;
+ foreach( QModelIndex index, selectedRows )
+ list << proxyModel->mapToSource( index );
+ selectedRows = list;
+ }
+
+ foreach( QModelIndex index, selectedRows )
+ {
+ int row = index.row();
+ DicRecord record = index.data( DictTableModel::DicRecordRole ).value<DicRecord>();
+ Q_ASSERT( !index.data( DictTableModel::DicRecordRole ).isNull() );
+ m_records.insert( row, new DicRecord( record ) );
+ recordsNum++;
+ }
+ setText(QObject::tr( "Remove %n record(s)", "Undo action of removing records", recordsNum ));
+ }
+
+RemoveRecordsCmd::~RemoveRecordsCmd()
+ {
+ qDeleteAll( m_records );
+ }
+
+void RemoveRecordsCmd::redo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
+
+void RemoveRecordsCmd::undo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+//==============================================================================================================
+
+EditRecordCmd::EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue ):
+ m_dictModel( aTableModel ), m_insertedRow( -1 )
+ {
+ m_row = aIndex.row();
+ m_col = aIndex.column();
+ m_oldStr = aIndex.data( Qt::EditRole ).toString();
+ m_newStr = aValue.toString();
+ if( m_row == m_dictModel->rowCount() - 1 && m_col == m_dictModel->columnCount() - 1 )
+ m_insertedRow = m_dictModel->rowCount();
+ QString editStr = m_newStr;
+ if( editStr.length() > MaxMenuEditStrLen )
+ editStr = editStr.left( MaxMenuEditStrLen ) + QChar( 8230 );
+ setText( QObject::tr( "Edit \"%1\"", "Undo action of editing a record").arg( editStr ) );
+ }
+
+void EditRecordCmd::redo()
+ {
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_newStr, Qt::EditRole );
+ if( m_insertedRow > -1 )
+ m_dictModel->insertRows( m_insertedRow, 1 );
+ }
+
+void EditRecordCmd::undo()
+ {
+ if( m_insertedRow > -1 )
+ m_dictModel->removeRows( m_insertedRow, 1 );
+ QModelIndex index = m_dictModel->index( m_row, m_col );
+ m_dictModel->setData( index, m_oldStr, Qt::EditRole );
+ }
+
+//==============================================================================================================
+
+PasteRecordsCmd::PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin ):
+ UndoRecordCmd( aMainWin )
+ {
+ const DictTableView* dictView = getCurView();
+ m_dictModel = dictView->dicTableModel(); // Always original model
+
+ // Find position where to insert records
+ // Paste above selection. If no selection -> paste in the end of dictionary
+ QModelIndexList selectedRows = dictView->selectionModel()->selectedRows();
+ int pastePos = m_dictModel->rowCount();
+ if( !selectedRows.isEmpty() )
+ {
+ qSort( selectedRows );
+ QModelIndex posIndex = selectedRows.first();
+
+ // If proxy view, convert position to source index
+ QAbstractProxyModel* proxyModel = qobject_cast<QAbstractProxyModel*>( dictView->model() );
+ if( proxyModel )
+ posIndex = proxyModel->mapToSource( posIndex );
+
+ pastePos = posIndex.row();
+ }
+ for( int i = 0; i < aRecords.size(); i++ )
+ m_records.insert( pastePos + i, aRecords.value( i ) );
+
+ setText(QObject::tr("Paste %n record(s)", "Undo action of pasting records", m_records.size() ));
+ }
+
+PasteRecordsCmd::~PasteRecordsCmd()
+ {
+ qDeleteAll( m_records.values() );
+ }
+
+void PasteRecordsCmd::redo()
+ {
+ insertRows( m_records.keys() );
+ setRecords( m_records );
+ selectRows( m_records.keys() );
+ }
+
+void PasteRecordsCmd::undo()
+ {
+ removeRows( m_records.keys() );
+ selectRow( m_records.keys().first() );
+ }
diff --git a/src/main-view/UndoCommands.h b/src/main-view/UndoCommands.h
new file mode 100644
index 0000000..a4affec
--- /dev/null
+++ b/src/main-view/UndoCommands.h
@@ -0,0 +1,109 @@
+#ifndef UNDOCOMMANDS_H
+#define UNDOCOMMANDS_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class DicRecord;
+class MainWindow;
+class DictTableView;
+class DictTableModel;
+class CardFilterModel;
+
+class UndoRecordCmd: public QUndoCommand
+{
+public:
+ UndoRecordCmd( const MainWindow* aMainWin );
+
+protected:
+ const DictTableView* getCurView();
+ CardFilterModel* getProxyModel( const DictTableView* aCurDictView );
+
+ void insertRows( QList<int> aRowNumbers );
+ void setRecords( QMap<int, DicRecord*> aRecords );
+ void removeRows( QList<int> aRowNumbers );
+ void selectRows( QList<int> aRowNumbers );
+ void selectRow( int aRow );
+
+protected:
+ const MainWindow* m_mainWindow;
+ DictTableModel* m_dictModel;
+};
+
+class InsertRecordsCmd: public UndoRecordCmd
+{
+public:
+ InsertRecordsCmd(const MainWindow* aMainWin);
+
+ static const int Id = 1;
+
+ void updateText();
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* command );
+ int id() const { return Id; }
+
+private:
+ QList<int> m_rowNumbers;
+};
+
+class RemoveRecordsCmd: public UndoRecordCmd
+{
+public:
+ RemoveRecordsCmd( const MainWindow* aMainWin );
+ ~RemoveRecordsCmd();
+
+ static const int Id = 2;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+ // The first element is the top row
+};
+
+/// Doesn't inherit UndoRecordCmd!
+class EditRecordCmd: public QUndoCommand
+{
+public:
+ EditRecordCmd( QAbstractItemModel* aTableModel, const QModelIndex& aIndex, const QVariant& aValue );
+
+ static const int Id = 3;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ static const int MaxMenuEditStrLen = 20;
+
+ QAbstractItemModel* m_dictModel;
+ int m_row;
+ int m_col;
+ QString m_oldStr;
+ QString m_newStr;
+ int m_insertedRow;
+};
+
+class PasteRecordsCmd: public UndoRecordCmd
+{
+public:
+ PasteRecordsCmd( QList<DicRecord*> aRecords, const MainWindow* aMainWin );
+ ~PasteRecordsCmd();
+
+ static const int Id = 4;
+
+ void redo();
+ void undo();
+ bool mergeWith( const QUndoCommand* ) { return false; }
+ int id() const { return Id; }
+
+private:
+ QMap<int, DicRecord*> m_records; // row number -> pointer to dic record. Sorted by key: rows
+};
+
+#endif // UNDOCOMMANDS_H
diff --git a/src/main-view/WelcomeScreen.cpp b/src/main-view/WelcomeScreen.cpp
new file mode 100644
index 0000000..806a57a
--- /dev/null
+++ b/src/main-view/WelcomeScreen.cpp
@@ -0,0 +1,51 @@
+#include "WelcomeScreen.h"
+
+WelcomeScreen::WelcomeScreen(QWidget* parent):
+ QWidget(parent)
+{
+ QPushButton* newButton = createWelcomeButton("filenew",
+ tr("Create new dictionary"), SLOT(newFile()));
+ QPushButton* openButton = createWelcomeButton("fileopen",
+ tr("Open existing dictionary"), SLOT(openFileWithDialog()));
+ QPushButton* openExampleButton = createWelcomeButton("",
+ tr("Open online dictionaries"), SLOT(openOnlineDictionaries()));
+ QPushButton* importButton = createWelcomeButton("",
+ tr("Import from CSV file"), SLOT(importFromCsv()));
+ recentFilesButton = createWelcomeButton("",
+ tr("Recent dictionaries"), NULL);
+
+ QVBoxLayout* buttonsLt = new QVBoxLayout;
+ buttonsLt->addWidget(newButton);
+ buttonsLt->addWidget(openButton);
+ buttonsLt->addWidget(openExampleButton);
+ buttonsLt->addWidget(importButton);
+ buttonsLt->addWidget(recentFilesButton);
+
+ QGridLayout* paddingLt = new QGridLayout;
+ paddingLt->addLayout(buttonsLt, 1, 1);
+ setLayout(paddingLt);
+}
+
+QPushButton* WelcomeScreen::createWelcomeButton(const QString& iconName,
+ const QString& text, const char* slot)
+{
+ static const QSize ButtonSize(350, 50);
+ QPushButton* button = new QPushButton(QIcon(QString(":/images/%1.png").
+ arg(iconName)), text);
+ button->setMaximumSize(ButtonSize);
+ if(slot)
+ connect(button, SIGNAL(clicked()), parentWidget(), slot);
+ return button;
+}
+
+void WelcomeScreen::updateRecentFilesButton()
+{
+ recentFilesButton->setEnabled(recentFilesButton->menu() &&
+ !recentFilesButton->menu()->actions().isEmpty());
+}
+
+void WelcomeScreen::setRecentFilesMenu(QMenu* menu)
+{
+ recentFilesButton->setMenu(menu);
+ updateRecentFilesButton();
+}
diff --git a/src/main-view/WelcomeScreen.h b/src/main-view/WelcomeScreen.h
new file mode 100644
index 0000000..17ca18e
--- /dev/null
+++ b/src/main-view/WelcomeScreen.h
@@ -0,0 +1,25 @@
+#ifndef WELCOME_SCREEN_H
+#define WELCOME_SCREEN_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class WelcomeScreen: public QWidget
+{
+ Q_OBJECT
+
+public:
+ WelcomeScreen(QWidget* parent);
+ void setRecentFilesMenu(QMenu* menu);
+
+public slots:
+ void updateRecentFilesButton();
+
+private:
+ QPushButton* createWelcomeButton(const QString& iconName, const QString& text,
+ const char* slot);
+
+private:
+ QPushButton* recentFilesButton;
+};
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..569c31a
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,102 @@
+#include <stdlib.h>
+#include <iostream>
+
+#include "main.h"
+#include "version.h"
+#include "strings.h"
+#include "main-view/AppModel.h"
+#include "main-view/MainWindow.h"
+
+int main(int argc, char *argv[])
+{
+ setlocale(LC_ALL, "en_US.UTF-8");
+ QApplication app(argc, argv);
+ initSettings();
+ setTranslations();
+
+ QString dictFilePath = "";
+ BuildStr = Strings::tr(Strings::s_build) + ": " + BUILD_REVISION;
+
+ if(processParams(dictFilePath))
+ return 0;
+
+ Q_INIT_RESOURCE(application);
+ AppModel model;
+ MainWindow mainWin(&model);
+ mainWin.show();
+ if(!dictFilePath.isEmpty())
+ mainWin.openFile(dictFilePath);
+ return app.exec();
+}
+
+void initSettings()
+{
+ QSettings::setDefaultFormat(QSettings::IniFormat);
+ QCoreApplication::setOrganizationName("freshmemory");
+ QCoreApplication::setApplicationName("freshmemory");
+}
+
+void setTranslations()
+{
+ QString locale = QSettings().value("lang").toString();
+ if(locale.isEmpty())
+ locale = QLocale::system().name();
+ installTranslator("qt_" + locale, QLibraryInfo::location(QLibraryInfo::TranslationsPath));
+ installTranslator("freshmemory_" + locale, getResourcePath() + "/tr");
+}
+
+void installTranslator(const QString& fileName, const QString& path)
+{
+ QTranslator* translator = new QTranslator;
+ translator->load(fileName, path);
+ qApp->installTranslator(translator);
+}
+
+QString getResourcePath()
+{
+#if defined(Q_OS_LINUX) || defined(Q_OS_MAC)
+ return "/usr/share/freshmemory";
+#else
+ return QCoreApplication::applicationDirPath();
+#endif
+}
+
+bool processParams(QString& dictFilePath)
+{
+ QStringList argList = QCoreApplication::arguments();
+ QListIterator<QString> argIter = argList;
+ argIter.next(); // The first argument is the program calling full name
+ while( argIter.hasNext() )
+ {
+ QString argStr = argIter.next();
+ if( argStr == "--version" || argStr == "-v" )
+ {
+ QString formattedBuildStr;
+ if( !BuildStr.isEmpty() )
+ formattedBuildStr = BuildStr + "\n";
+
+ std::cout << (Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "\n" +
+ formattedBuildStr +
+ Strings::tr(Strings::s_author) + "\n" +
+ MainWindow::tr("Website:") + " http://fresh-memory.com\n").toStdString();
+ return true;
+ }
+ else if( argStr == "--help" || argStr == "-h" )
+ {
+ std::cout <<
+ (MainWindow::tr("Usage:") + " freshmemory [OPTIONS] [FILE]\n" +
+ MainWindow::tr("FILE is a dictionary filename to load.") +"\n\n" +
+ MainWindow::tr("Options:") + "\n" +
+ "\t-h, --help\t" + MainWindow::tr("Display this help and exit") + "\n" +
+ "\t-v, --version\t" + MainWindow::tr("Output version information and exit") + "\n")
+ .toStdString();
+ return true;
+ }
+ else if( !argIter.hasNext() ) // the last argument
+ {
+ dictFilePath = argStr;
+ return false;
+ }
+ }
+ return false;
+}
diff --git a/src/main.h b/src/main.h
new file mode 100644
index 0000000..b4922c3
--- /dev/null
+++ b/src/main.h
@@ -0,0 +1,8 @@
+#include <QtCore>
+#include <QtWidgets>
+
+void initSettings();
+void setTranslations();
+void installTranslator(const QString& fileName, const QString& path);
+QString getResourcePath();
+bool processParams(QString& dictFilePath);
diff --git a/src/settings/ColorBox.cpp b/src/settings/ColorBox.cpp
new file mode 100644
index 0000000..4ed73ab
--- /dev/null
+++ b/src/settings/ColorBox.cpp
@@ -0,0 +1,36 @@
+#include "ColorBox.h"
+
+ColorBox::ColorBox( QColor aColor )
+ {
+ setAutoFillBackground(true);
+ setFrameStyle( QFrame::Box );
+ setLineWidth(2);
+ setMinimumHeight( MinHeight );
+ setMinimumWidth( MinWidth );
+ setColor( aColor );
+ }
+
+void ColorBox::setColor( QColor aColor )
+ {
+ m_color = aColor;
+ if( isEnabled() )
+ setPalette( m_color );
+ else
+ setPalette( Qt::lightGray );
+ }
+
+void ColorBox::mousePressEvent(QMouseEvent* /*event*/)
+ {
+ QColor c = QColorDialog::getColor( color(), this );
+ if( c.isValid() )
+ {
+ setColor( c );
+ emit colorChanged( c );
+ }
+ }
+
+void ColorBox::changeEvent ( QEvent* event )
+ {
+ if( event->type() == QEvent::EnabledChange )
+ setColor( m_color );
+ }
diff --git a/src/settings/ColorBox.h b/src/settings/ColorBox.h
new file mode 100644
index 0000000..5844610
--- /dev/null
+++ b/src/settings/ColorBox.h
@@ -0,0 +1,28 @@
+#ifndef COLOR_BOX_H
+#define COLOR_BOX_H
+
+#include <QtWidgets>
+
+class ColorBox: public QFrame
+{
+ Q_OBJECT
+
+public:
+ ColorBox( QColor aColor = Qt::white );
+ QColor color() const { return m_color; }
+ void setColor( QColor aColor );
+
+protected:
+ void mousePressEvent ( QMouseEvent* event );
+ void changeEvent ( QEvent* event );
+
+signals:
+ void colorChanged( QColor aColor );
+
+private:
+ static const int MinHeight = 25;
+ static const int MinWidth = 50;
+ QColor m_color;
+};
+
+#endif
diff --git a/src/settings/FontColorSettingsDialog.cpp b/src/settings/FontColorSettingsDialog.cpp
new file mode 100644
index 0000000..beb5f01
--- /dev/null
+++ b/src/settings/FontColorSettingsDialog.cpp
@@ -0,0 +1,300 @@
+#include "FontColorSettingsDialog.h"
+#include "ColorBox.h"
+#include "../dic-options/FieldsPreviewModel.h"
+#include "StylesListModel.h"
+#include "StylePreviewModel.h"
+
+#include <QtCore>
+
+FontColorSettingsDialog::FontColorSettingsDialog(QWidget *parent) :
+ QDialog(parent), m_curStyle( NULL )
+ {
+ initData();
+ QHBoxLayout* bgColorLt = createBgColorSelector();
+ QLabel* stylesLbl = createStylesList();
+ QVBoxLayout* styleLt = createStyleControls();
+ createKeywordBox( styleLt );
+ styleLt->addStretch();
+ QLabel* previewLbl = createStylePreview();
+
+ m_okCancelBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel |
+ QDialogButtonBox::RestoreDefaults, Qt::Horizontal );
+ connect( m_okCancelBox, SIGNAL(accepted()), this, SLOT(accept()) );
+ connect( m_okCancelBox, SIGNAL(rejected()), this, SLOT(reject()) );
+ connect( m_okCancelBox, SIGNAL(clicked(QAbstractButton*)), SLOT(dialogButtonClicked(QAbstractButton*)) );
+
+ QGridLayout* mainLt = new QGridLayout;
+ mainLt->addLayout( bgColorLt, 0, 0, 1, 2 );
+ mainLt->addWidget( stylesLbl, 1, 0 );
+ mainLt->addWidget( m_stylesListView, 2, 0 );
+ mainLt->addLayout( styleLt, 2, 1 );
+ mainLt->addWidget( previewLbl, 1, 2 );
+ mainLt->addWidget( m_stylesPreview, 2, 2 );
+ mainLt->addWidget( m_okCancelBox, 3, 0, 1, 3 );
+
+ setLayout( mainLt );
+ setWindowTitle( tr("Font & color settings") );
+
+ m_stylesListView->setCurrentIndex( m_stylesListView->model()->index(0,0) );
+ }
+
+FontColorSettingsDialog::~FontColorSettingsDialog()
+ {
+ delete m_styleFactory;
+ }
+
+QHBoxLayout* FontColorSettingsDialog::createBgColorSelector()
+ {
+ m_bgColorSelector = new ColorBox( m_styleFactory->cardBgColor );
+ connect( m_bgColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setBgColor(QColor)) );
+
+ QHBoxLayout* bgColorLt = new QHBoxLayout;
+ bgColorLt->addWidget( new QLabel(tr("Card background color:")) );
+ bgColorLt->addWidget( m_bgColorSelector );
+ bgColorLt->addStretch();
+
+ return bgColorLt;
+ }
+
+QLabel* FontColorSettingsDialog::createStylesList()
+ {
+ QLabel* stylesLbl = new QLabel("<b>"+tr("Field styles")+"</b>");
+
+ QStringList styleNames = FieldStyleFactory::inst()->getStyleNames();
+ StylesListModel* stylesListModel = new StylesListModel( styleNames );
+ m_stylesListView = new QListView;
+ m_stylesListView->setModel( stylesListModel );
+ m_stylesListView->setMaximumWidth( StyleListMaxWidth );
+
+ connect( m_stylesListView->selectionModel(),
+ SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
+ SLOT(updateStyleControls(const QModelIndex&)) );
+
+ return stylesLbl;
+ }
+
+QVBoxLayout* FontColorSettingsDialog::createStyleControls()
+ {
+ m_fontSelector = new QFontComboBox;
+ m_fontSelector->setMaximumWidth( 180 );
+ connect( m_fontSelector, SIGNAL(currentFontChanged(QFont)), SLOT(setFontFamily(QFont)) );
+
+ m_sizeSelector = new QSpinBox;
+ m_sizeSelector->setMaximumWidth( SizeSelectorMaxWidth );
+ m_sizeSelector->setMinimum( 1 );
+ m_sizeSelector->setMaximum( 40 );
+ connect( m_sizeSelector, SIGNAL(valueChanged(int)), SLOT(setFontSize(int)) );
+ QVBoxLayout* styleLt = new QVBoxLayout;
+
+
+ QVBoxLayout* familyLt = new QVBoxLayout;
+ familyLt->addWidget( new QLabel(tr("Font family:")) );
+ familyLt->addWidget( m_fontSelector );
+ QVBoxLayout* sizeLt = new QVBoxLayout;
+ sizeLt->addWidget( new QLabel(tr("Size:")) );
+ sizeLt->addWidget( m_sizeSelector );
+ QHBoxLayout* lt0 = new QHBoxLayout;
+ lt0->setContentsMargins( QMargins() );
+ lt0->addLayout( familyLt );
+ lt0->addLayout( sizeLt );
+ lt0->addStretch();
+
+ m_fontColorSelector = new ColorBox;
+ m_boldCB = new QCheckBox(tr("Bold"));
+ m_italicCB = new QCheckBox(tr("Italic"));
+ QHBoxLayout* lt1 = new QHBoxLayout;
+ lt1->addWidget( new QLabel(tr("Color:")) );
+ lt1->addWidget( m_fontColorSelector );
+ lt1->addWidget( m_boldCB );
+ lt1->addWidget( m_italicCB );
+ lt1->addStretch();
+ connect( m_fontColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setStyleColor(QColor)) );
+ connect( m_boldCB, SIGNAL(stateChanged(int)), SLOT(setBoldFont(int)) );
+ connect( m_italicCB, SIGNAL(stateChanged(int)), SLOT(setItalicFont(int)) );
+
+ m_prefixEdit = new QLineEdit;
+ m_prefixEdit->setMaximumWidth( StyleEditMaxWidth );
+ m_suffixEdit = new QLineEdit;
+ m_suffixEdit->setMaximumWidth( StyleEditMaxWidth );
+ connect( m_prefixEdit, SIGNAL(textChanged(const QString&)), SLOT(setPrefix(const QString)) );
+ connect( m_suffixEdit, SIGNAL(textChanged(const QString&)), SLOT(setSuffix(const QString)) );
+
+ QHBoxLayout* lt2 = new QHBoxLayout;
+ lt2->addWidget( new QLabel(tr("Prefix:")) );
+ lt2->addWidget( m_prefixEdit );
+ lt2->addWidget( new QLabel(tr("Suffix:")) );
+ lt2->addWidget( m_suffixEdit );
+ lt2->addStretch();
+
+ styleLt->addLayout( lt0 );
+ styleLt->addLayout( lt1 );
+ styleLt->addLayout( lt2 );
+ return styleLt;
+ }
+
+void FontColorSettingsDialog::createKeywordBox( QVBoxLayout* aStyleLt )
+ {
+ m_keywordBox = new QGroupBox(tr("Keyword style"));
+ m_keywordBox->setCheckable(true);
+ connect( m_keywordBox, SIGNAL(toggled(bool)), SLOT(setKeywordStyle(bool)) );
+
+ m_keywordColorSelector = new ColorBox();
+ m_keywordBoldCB = new QCheckBox(tr("Bold"));
+ m_keywordItalicCB = new QCheckBox(tr("Italic"));
+ connect( m_keywordBoldCB, SIGNAL(stateChanged(int)), SLOT(setKeywordBoldFont(int)) );
+ connect( m_keywordItalicCB, SIGNAL(stateChanged(int)), SLOT(setKeywordItalicFont(int)) );
+ connect( m_keywordColorSelector, SIGNAL(colorChanged(QColor)), SLOT(setKeywordColor(QColor)) );
+
+ QHBoxLayout* keywordLt1 = new QHBoxLayout;
+ keywordLt1->addWidget( new QLabel(tr("Color:")) );
+ keywordLt1->addWidget( m_keywordColorSelector );
+ keywordLt1->addWidget( m_keywordBoldCB );
+ keywordLt1->addWidget( m_keywordItalicCB );
+ keywordLt1->addStretch();
+
+ m_keywordBox->setLayout( keywordLt1 );
+ aStyleLt->addWidget( m_keywordBox );
+ }
+
+QLabel* FontColorSettingsDialog::createStylePreview()
+ {
+ QLabel* previewLbl = new QLabel("<b>"+tr("Style preview")+"</b>");
+ StylePreviewModel* stylesPreviewModel = new StylePreviewModel( this );
+ m_stylesPreview = new QTableView;
+ m_stylesPreview->setModel( stylesPreviewModel );
+ m_stylesPreview->verticalHeader()->hide();
+ m_stylesPreview->horizontalHeader()->hide();
+ m_stylesPreview->setShowGrid( false );
+ updatePreview();
+ return previewLbl;
+ }
+
+void FontColorSettingsDialog::initData()
+ {
+ m_styleFactory = new FieldStyleFactory( *FieldStyleFactory::inst() );
+ }
+
+void FontColorSettingsDialog::updateStyleControls( const QModelIndex& aIndex )
+{
+ QString styleName = m_styleFactory->getStyleNames().value( aIndex.row() );
+ m_curStyle = m_styleFactory->getStylePtr( styleName );
+
+ m_fontSelector->setCurrentFont( m_curStyle->font );
+ m_sizeSelector->setValue( m_curStyle->font.pointSize() );
+ m_boldCB->setChecked( m_curStyle->font.bold() );
+ m_italicCB->setChecked( m_curStyle->font.italic() );
+ m_fontColorSelector->setColor( m_curStyle->color );
+ m_prefixEdit->setText( m_curStyle->prefix );
+ m_suffixEdit->setText( m_curStyle->suffix );
+
+ // Keyword style
+ m_keywordBox->setChecked(m_curStyle->hasKeyword);
+ m_keywordBoldCB->setChecked( m_curStyle->keywordBold );
+ m_keywordItalicCB->setChecked( m_curStyle->keywordItalic );
+ m_keywordColorSelector->setColor( m_curStyle->keywordColor );
+}
+
+void FontColorSettingsDialog::updatePreview()
+ {
+ m_stylesPreview->reset();
+ m_stylesPreview->resizeColumnsToContents();
+ m_stylesPreview->resizeRowsToContents();
+ }
+
+void FontColorSettingsDialog::setBgColor( QColor aColor )
+ {
+ m_styleFactory->cardBgColor = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setFontFamily( QFont aFont )
+ {
+ m_curStyle->font.setFamily( aFont.family() );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setFontSize( int aSize )
+ {
+ m_curStyle->font.setPointSize( aSize );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setBoldFont( int aState )
+ {
+ m_curStyle->font.setBold( aState == Qt::Checked );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setItalicFont( int aState )
+ {
+ m_curStyle->font.setItalic( aState == Qt::Checked );
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setStyleColor( QColor aColor )
+ {
+ m_curStyle->color = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setPrefix( const QString aText )
+ {
+ m_curStyle->prefix = aText;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setSuffix( const QString aText )
+ {
+ m_curStyle->suffix = aText;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordStyle( bool aNewKeywordStyleState )
+ {
+ m_curStyle->hasKeyword = aNewKeywordStyleState;
+ if(aNewKeywordStyleState)
+ {
+ m_keywordBoldCB->setChecked(m_curStyle->keywordBold);
+ m_keywordItalicCB->setChecked(m_curStyle->keywordItalic);
+ m_keywordColorSelector->setColor(m_curStyle->keywordColor);
+ }
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordBoldFont( int aState )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordBold = (aState == Qt::Checked);
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordItalicFont( int aState )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordItalic = (aState == Qt::Checked);
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::setKeywordColor( QColor aColor )
+ {
+ if(!m_curStyle->hasKeyword)
+ return;
+ m_curStyle->keywordColor = aColor;
+ updatePreview();
+ }
+
+void FontColorSettingsDialog::dialogButtonClicked( QAbstractButton* aButton )
+ {
+ if( m_okCancelBox->buttonRole( aButton ) == QDialogButtonBox::ResetRole )
+ {
+ delete m_styleFactory;
+ m_styleFactory = new FieldStyleFactory;
+ m_bgColorSelector->setColor( m_styleFactory->cardBgColor );
+ QModelIndex index = m_stylesListView->model()->index(0,0);
+ m_stylesListView->setCurrentIndex( index );
+ updateStyleControls( index );
+ updatePreview();
+ }
+ }
diff --git a/src/settings/FontColorSettingsDialog.h b/src/settings/FontColorSettingsDialog.h
new file mode 100644
index 0000000..273f61c
--- /dev/null
+++ b/src/settings/FontColorSettingsDialog.h
@@ -0,0 +1,71 @@
+#ifndef FONTCOLORSETTINGSDIALOG_H
+#define FONTCOLORSETTINGSDIALOG_H
+
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QtWidgets>
+
+class ColorBox;
+
+class FontColorSettingsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ FontColorSettingsDialog(QWidget *parent = 0);
+ ~FontColorSettingsDialog();
+
+ const FieldStyleFactory* styleFactory() const { return m_styleFactory; }
+
+private:
+ void initData();
+ QHBoxLayout* createBgColorSelector();
+ QLabel* createStylesList();
+ QPushButton* createRestoreButton();
+ QVBoxLayout* createStyleControls();
+ void createKeywordBox( QVBoxLayout* aStyleLt );
+ QLabel* createStylePreview();
+ void updatePreview();
+
+private slots:
+ void updateStyleControls( const QModelIndex& aIndex );
+ void setBgColor(QColor aColor);
+ void setFontFamily(QFont aFont);
+ void setFontSize(int aSize);
+ void setBoldFont(int aState);
+ void setItalicFont(int aState);
+ void setStyleColor(QColor aColor);
+ void setPrefix(const QString aText);
+ void setSuffix(const QString aText);
+ void setKeywordStyle(bool aNewKeywordStyleState);
+ void setKeywordBoldFont(int aState);
+ void setKeywordItalicFont(int aState);
+ void setKeywordColor(QColor aColor);
+ void dialogButtonClicked( QAbstractButton* aButton );
+
+private:
+ static const int StyleListMaxWidth = 150;
+ static const int SizeSelectorMaxWidth = 50;
+ static const int StyleEditMaxWidth = 40;
+
+ FieldStyleFactory* m_styleFactory;
+ FieldStyle* m_curStyle;
+
+ ColorBox* m_bgColorSelector;
+ QListView* m_stylesListView;
+ QFontComboBox* m_fontSelector;
+ QSpinBox* m_sizeSelector;
+ QCheckBox* m_boldCB;
+ QCheckBox* m_italicCB;
+ ColorBox* m_fontColorSelector;
+ QLineEdit* m_prefixEdit;
+ QLineEdit* m_suffixEdit;
+ QGroupBox* m_keywordBox;
+ QCheckBox* m_keywordBoldCB;
+ QCheckBox* m_keywordItalicCB;
+ ColorBox* m_keywordColorSelector;
+ QTableView* m_stylesPreview;
+ QDialogButtonBox* m_okCancelBox;
+};
+
+#endif
diff --git a/src/settings/StudySettingsDialog.cpp b/src/settings/StudySettingsDialog.cpp
new file mode 100644
index 0000000..b38bf67
--- /dev/null
+++ b/src/settings/StudySettingsDialog.cpp
@@ -0,0 +1,141 @@
+#include "StudySettingsDialog.h"
+
+StudySettingsDialog::StudySettingsDialog(QWidget *parent):
+ QDialog(parent)
+ {
+ initData();
+ createUi();
+ updateControls();
+ }
+
+void StudySettingsDialog::initData()
+ {
+ settings = *StudySettings::inst();
+ }
+
+void StudySettingsDialog::createUi()
+{
+ createControls();
+ setLayout(createMainLayout());
+ setWindowTitle(tr("Study settings"));
+}
+
+void StudySettingsDialog::createControls()
+{
+ dayShiftBox = createSpinBox(0, 12);
+ newCardsShareBox = createSpinBox(1, 99);
+ randomnessBox = createSpinBox(1, 99);
+ cardsDayLimitBox = createSpinBox(1, 500);
+ newCardsDayLimitBox = createSpinBox(0, 200);
+ limitForAddingNewCardsBox = createSpinBox(20, 500);
+ showRandomlyCB = new QCheckBox(tr("Add new cards in random order"));
+ createButtonBox();
+}
+
+QSpinBox* StudySettingsDialog::createSpinBox(int min, int max)
+{
+ QSpinBox* spinBox = new QSpinBox;
+ spinBox->setMinimum(min);
+ spinBox->setMaximum(max);
+ return spinBox;
+}
+
+void StudySettingsDialog::createButtonBox()
+{
+ buttonBox = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel |
+ QDialogButtonBox::RestoreDefaults, Qt::Horizontal);
+ connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+ connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+ connect(buttonBox, SIGNAL(clicked(QAbstractButton*)),
+ SLOT(dialogButtonClicked(QAbstractButton*)));
+}
+
+QBoxLayout* StudySettingsDialog::createMainLayout()
+{
+ QHBoxLayout* upperPaddedLt = new QHBoxLayout;
+ upperPaddedLt->addLayout(createUpperLayout());
+ upperPaddedLt->addStretch();
+
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addLayout(upperPaddedLt);
+ mainLt->addWidget(showRandomlyCB);
+ mainLt->addWidget(createLimitsGroup());
+ mainLt->addWidget(buttonBox);
+ return mainLt;
+}
+
+QGridLayout* StudySettingsDialog::createUpperLayout()
+{
+ QGridLayout* layout = new QGridLayout;
+ int row = 0;
+ addUpperGridLine(layout, row++, tr("Day starts at, o'clock:"), dayShiftBox);
+ addUpperGridLine(layout, row++, tr("Share of new cards:"), newCardsShareBox, "%");
+ addUpperGridLine(layout, row++, tr("Repetition interval randomness:"), randomnessBox, "%");
+ return layout;
+}
+
+void StudySettingsDialog::addUpperGridLine(QGridLayout* layout, int row, const QString& label,
+ QWidget* widget, const QString& unitLabel)
+{
+ QLabel* labelWidget = new QLabel(label);
+ layout->addWidget(labelWidget, row, 0);
+ layout->addWidget(widget, row, 1);
+ layout->addWidget(new QLabel(unitLabel), row, 2);
+}
+
+QGroupBox* StudySettingsDialog::createLimitsGroup()
+{
+ QGridLayout* layout = new QGridLayout;
+ layout->setColumnStretch(0, 1);
+ int row = 0;
+ addLimitsGridLine(layout, row++, tr("Day reviews limit:"), cardsDayLimitBox);
+ addLimitsGridLine(layout, row++, tr("Day limit of new cards:"), newCardsDayLimitBox);
+ addLimitsGridLine(layout, row++,
+ tr("Don't add new cards after scheduled cards threshold:"),
+ limitForAddingNewCardsBox);
+ QGroupBox* group = new QGroupBox(tr("Limits"));
+ group->setLayout(layout);
+ return group;
+}
+
+void StudySettingsDialog::addLimitsGridLine(QGridLayout* layout, int row, const QString& label,
+ QWidget* widget)
+{
+ QLabel* labelWidget = new QLabel(label);
+ labelWidget->setWordWrap(true);
+ layout->addWidget(labelWidget, row, 0);
+ layout->addWidget(widget, row, 1);
+}
+
+const StudySettings StudySettingsDialog::getSettings()
+{
+ settings.showRandomly = showRandomlyCB->isChecked();
+ settings.newCardsShare = newCardsShareBox->value() / 100.;
+ settings.schedRandomness = randomnessBox->value() / 100.;
+ settings.cardsDayLimit = cardsDayLimitBox->value();
+ settings.newCardsDayLimit = newCardsDayLimitBox->value();
+ settings.limitForAddingNewCards = limitForAddingNewCardsBox->value();
+ settings.dayShift = dayShiftBox->value();
+ return settings;
+}
+
+void StudySettingsDialog::updateControls()
+{
+ showRandomlyCB->setChecked( settings.showRandomly );
+ newCardsShareBox->setValue( settings.newCardsShare * 100 );
+ randomnessBox->setValue( settings.schedRandomness * 100 );
+ cardsDayLimitBox->setValue( settings.cardsDayLimit );
+ newCardsDayLimitBox->setValue( settings.newCardsDayLimit );
+ limitForAddingNewCardsBox->setValue(settings.limitForAddingNewCards);
+ dayShiftBox->setValue( settings.dayShift );
+}
+
+void StudySettingsDialog::dialogButtonClicked( QAbstractButton* aButton )
+ {
+ if(buttonBox->buttonRole(aButton) == QDialogButtonBox::ResetRole)
+ {
+ settings = StudySettings();
+ updateControls();
+ }
+ }
diff --git a/src/settings/StudySettingsDialog.h b/src/settings/StudySettingsDialog.h
new file mode 100644
index 0000000..121767b
--- /dev/null
+++ b/src/settings/StudySettingsDialog.h
@@ -0,0 +1,45 @@
+#ifndef STUDYSETTINGSDIALOG_H
+#define STUDYSETTINGSDIALOG_H
+
+#include "../study/StudySettings.h"
+
+#include <QtWidgets>
+
+class StudySettingsDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ StudySettingsDialog(QWidget *parent = 0);
+ const StudySettings getSettings();
+
+private:
+ void initData();
+ void createUi();
+ void updateControls();
+ void addUpperGridLine(QGridLayout* layout, int row, const QString& label, QWidget* widget,
+ const QString& unitLabel = "");
+ void addLimitsGridLine(QGridLayout* layout, int row, const QString& label, QWidget* widget);
+ QSpinBox* createSpinBox(int min, int max);
+ void createButtonBox();
+ void createControls();
+ QBoxLayout* createMainLayout();
+ QGridLayout*createUpperLayout();
+ QGroupBox* createLimitsGroup();
+
+private slots:
+ void dialogButtonClicked( QAbstractButton* aButton );
+
+private:
+ StudySettings settings;
+ QSpinBox* dayShiftBox;
+ QCheckBox* showRandomlyCB;
+ QSpinBox* newCardsShareBox;
+ QSpinBox* randomnessBox;
+ QSpinBox* cardsDayLimitBox;
+ QSpinBox* newCardsDayLimitBox;
+ QSpinBox* limitForAddingNewCardsBox;
+ QDialogButtonBox* buttonBox;
+};
+
+#endif
diff --git a/src/settings/StylePreviewModel.cpp b/src/settings/StylePreviewModel.cpp
new file mode 100644
index 0000000..e07c809
--- /dev/null
+++ b/src/settings/StylePreviewModel.cpp
@@ -0,0 +1,63 @@
+#include "StylePreviewModel.h"
+#include "../field-styles/FieldStyleFactory.h"
+
+#include <QBrush>
+
+QVariant StylePreviewModel::data( const QModelIndex &index, int role ) const
+ {
+ if( !index.isValid())
+ return QVariant();
+ if( index.row() >= rowCount() || index.column() >= columnCount() )
+ return QVariant();
+
+ QString styleName = m_parent->styleFactory()->getStyleNames().value( index.row() );
+ FieldStyle fieldStyle = m_parent->styleFactory()->getStyle( styleName );
+ switch( index.column() )
+ {
+ case 0:
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return fieldStyle.prefix + styleName + fieldStyle.suffix;
+ case Qt::FontRole:
+ return fieldStyle.font;
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.color;
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ case 1:
+ if( fieldStyle.hasKeyword )
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return tr("keyword");
+ case Qt::FontRole:
+ return fieldStyle.getKeywordStyle().font;
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ case Qt::ForegroundRole:
+ return fieldStyle.keywordColor;
+ case Qt::TextAlignmentRole:
+ return Qt::AlignCenter;
+ default:
+ return QVariant();
+ }
+ else
+ switch( role )
+ {
+ case Qt::DisplayRole:
+ return QVariant();
+ case Qt::BackgroundRole:
+ return QBrush( m_parent->styleFactory()->cardBgColor );
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ }
diff --git a/src/settings/StylePreviewModel.h b/src/settings/StylePreviewModel.h
new file mode 100644
index 0000000..1987431
--- /dev/null
+++ b/src/settings/StylePreviewModel.h
@@ -0,0 +1,32 @@
+#ifndef STYLESPREVIEWMODEL_H
+#define STYLEPREVIEWMODEL_H
+
+#include "../field-styles/FieldStyleFactory.h"
+#include "FontColorSettingsDialog.h"
+
+#include <QAbstractTableModel>
+
+class StylePreviewModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ StylePreviewModel( FontColorSettingsDialog* aParent ):
+ m_parent( aParent )
+ {}
+
+ int rowCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return m_parent->styleFactory()->getStyleNames().size(); }
+ int columnCount( const QModelIndex& /*parent*/ = QModelIndex() ) const
+ { return 2; }
+ QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const;
+ QVariant headerData( int /*section*/, Qt::Orientation /*orientation*/, int /*role = Qt::DisplayRole*/ ) const
+ { return QVariant(); }
+ Qt::ItemFlags flags(const QModelIndex &/*index*/) const {return Qt::NoItemFlags;}
+
+private:
+ FontColorSettingsDialog* m_parent;
+
+};
+
+#endif
diff --git a/src/settings/StylesListModel.h b/src/settings/StylesListModel.h
new file mode 100644
index 0000000..3899ba8
--- /dev/null
+++ b/src/settings/StylesListModel.h
@@ -0,0 +1,16 @@
+#ifndef STYLESLISTMODEL_H
+#define STYLESLISTMODEL_H
+
+#include <QStringListModel>
+
+class StylesListModel: public QStringListModel
+{
+ Q_OBJECT
+
+public:
+ StylesListModel( const QStringList& aStrings ): QStringListModel( aStrings) {}
+ Qt::ItemFlags flags( const QModelIndex& index ) const
+ { return QAbstractListModel::flags(index); }
+};
+
+#endif // STYLESLISTMODEL_H
diff --git a/src/statistics/BaseStatPage.cpp b/src/statistics/BaseStatPage.cpp
new file mode 100644
index 0000000..a7a8764
--- /dev/null
+++ b/src/statistics/BaseStatPage.cpp
@@ -0,0 +1,38 @@
+#include "BaseStatPage.h"
+#include "StatisticsParams.h"
+
+BaseStatPage::BaseStatPage(const StatisticsParams* statParams):
+ statParams(statParams)
+{
+}
+
+void BaseStatPage::init()
+{
+ createUi();
+ updateDataSet();
+}
+
+void BaseStatPage::createUi()
+{
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addWidget(createTitleLabel());
+ mainLt->addWidget(createChart());
+ mainLt->addWidget(createTotalReviewsLabel());
+ setLayout(mainLt);
+}
+
+QLabel* BaseStatPage::createTitleLabel()
+{
+ QLabel* titleLabel = new QLabel(getTitle());
+ titleLabel->setFont(QFont("Sans Serif", 18, QFont::Bold));
+ titleLabel->setAlignment(Qt::AlignCenter);
+ return titleLabel;
+}
+
+QWidget* BaseStatPage::createTotalReviewsLabel()
+{
+ totalReviewsLabel = new QLabel;
+ totalReviewsLabel->setFont(QFont("Sans Serif", 16));
+ totalReviewsLabel->setAlignment(Qt::AlignRight);
+ return totalReviewsLabel;
+}
diff --git a/src/statistics/BaseStatPage.h b/src/statistics/BaseStatPage.h
new file mode 100644
index 0000000..7ef91bd
--- /dev/null
+++ b/src/statistics/BaseStatPage.h
@@ -0,0 +1,31 @@
+#ifndef STATISTICS_PAGE_H
+#define STATISTICS_PAGE_H
+
+#include <QtWidgets>
+
+class StatisticsParams;
+
+class BaseStatPage: public QWidget
+{
+ Q_OBJECT
+public:
+ BaseStatPage(const StatisticsParams* statParams);
+ virtual void updateDataSet() = 0;
+ virtual QString getTitle() const = 0;
+ virtual bool usesTimePeriod() const = 0;
+
+protected:
+ void init();
+ virtual QWidget* createChart() = 0;
+
+private:
+ void createUi();
+ QLabel* createTitleLabel();
+ QWidget* createTotalReviewsLabel();
+
+protected:
+ const StatisticsParams* statParams;
+ QLabel* totalReviewsLabel;
+};
+
+#endif
diff --git a/src/statistics/ProgressPage.cpp b/src/statistics/ProgressPage.cpp
new file mode 100644
index 0000000..7676173
--- /dev/null
+++ b/src/statistics/ProgressPage.cpp
@@ -0,0 +1,41 @@
+#include "ProgressPage.h"
+#include "StatisticsParams.h"
+#include "../charts/PieChart.h"
+#include "../dictionary/CardPack.h"
+
+ProgressPage::ProgressPage(const StatisticsParams* statParams):
+ BaseStatPage(statParams)
+{
+ init();
+}
+
+QWidget* ProgressPage::createChart()
+{
+ chart = new PieChart;
+ chart->setColors({"#39c900", "#ece900", "#ff0000"});
+ return chart;
+}
+
+void ProgressPage::updateDataSet()
+{
+ updateCardsNumbers();
+ totalReviewsLabel->setText(tr("Total: %1").arg(allCardsNum));
+ chart->setDataSet(getDataSet());
+}
+
+void ProgressPage::updateCardsNumbers()
+{
+ const CardPack* pack = statParams->getCardPack();
+ allCardsNum = pack->cardsNum();
+ newCardsNum = pack->getNewCards().size();
+ toBeRepeated = pack->getActiveCards().size();
+}
+
+QList<DataPoint> ProgressPage::getDataSet() const
+{
+ QList<DataPoint> dataSet;
+ dataSet << DataPoint(tr("Studied"), allCardsNum - newCardsNum - toBeRepeated);
+ dataSet << DataPoint(tr("Scheduled for today"), toBeRepeated);
+ dataSet << DataPoint(tr("New"), newCardsNum);
+ return dataSet;
+}
diff --git a/src/statistics/ProgressPage.h b/src/statistics/ProgressPage.h
new file mode 100644
index 0000000..9adb4cc
--- /dev/null
+++ b/src/statistics/ProgressPage.h
@@ -0,0 +1,33 @@
+#ifndef PROGRESS_PAGE_H
+#define PROGRESS_PAGE_H
+
+#include <QtWidgets>
+#include "BaseStatPage.h"
+#include "../charts/DataPoint.h"
+
+class PieChart;
+
+class ProgressPage: public BaseStatPage
+{
+ Q_OBJECT
+public:
+ ProgressPage(const StatisticsParams* statParams);
+ void updateDataSet();
+ QString getTitle() const { return tr("Study progress"); }
+ bool usesTimePeriod() const {return false;}
+
+protected:
+ QWidget* createChart();
+
+private:
+ void updateCardsNumbers();
+ QList<DataPoint> getDataSet() const;
+
+private:
+ PieChart* chart;
+ int allCardsNum;
+ int newCardsNum;
+ int toBeRepeated;
+};
+
+#endif
diff --git a/src/statistics/ScheduledPage.cpp b/src/statistics/ScheduledPage.cpp
new file mode 100644
index 0000000..5e2b558
--- /dev/null
+++ b/src/statistics/ScheduledPage.cpp
@@ -0,0 +1,35 @@
+#include "ScheduledPage.h"
+#include "../dictionary/CardPack.h"
+
+ScheduledPage::ScheduledPage(const StatisticsParams* statParams):
+ TimeChartPage(statParams)
+{
+ init();
+}
+
+QList<QDateTime> ScheduledPage::getDates(const CardPack* pack) const
+{
+ QList<QDateTime> scheduled = pack->getScheduledDates();
+ adjustScheduledRecords(scheduled);
+ return scheduled;
+}
+
+void ScheduledPage::adjustScheduledRecords(QList<QDateTime>& scheduled)
+{
+ const QDate curDate = QDate::currentDate();
+ const QTime zeroTime = QTime(0, 0);
+ for(int i = 0; i < scheduled.size(); i++)
+ {
+ QDateTime& dateTime = scheduled[i];
+ if(dateTime.date() < curDate)
+ dateTime = QDateTime(curDate, zeroTime);
+ else if(laterThisDay(dateTime))
+ dateTime = QDateTime(curDate.addDays(1), zeroTime);
+ }
+}
+
+bool ScheduledPage::laterThisDay(QDateTime& dateTime)
+{
+ return dateTime.date() == QDate::currentDate() &&
+ dateTime > QDateTime::currentDateTime();
+}
diff --git a/src/statistics/ScheduledPage.h b/src/statistics/ScheduledPage.h
new file mode 100644
index 0000000..f5a4eec
--- /dev/null
+++ b/src/statistics/ScheduledPage.h
@@ -0,0 +1,22 @@
+#ifndef SCHEDULED_PAGE_H
+#define SCHEDULED_PAGE_H
+
+#include "TimeChartPage.h"
+
+class ScheduledPage: public TimeChartPage
+{
+ Q_OBJECT
+public:
+ ScheduledPage(const StatisticsParams* statParams);
+ QString getTitle() const { return tr("Scheduled cards"); }
+
+protected:
+ QList<QDateTime> getDates(const CardPack* pack) const;
+ int getDataDirection() const { return 1; }
+
+private:
+ static void adjustScheduledRecords(QList<QDateTime>& scheduled);
+ static bool laterThisDay(QDateTime& dateTime);
+};
+
+#endif
diff --git a/src/statistics/StatisticsParams.cpp b/src/statistics/StatisticsParams.cpp
new file mode 100644
index 0000000..4e5d80f
--- /dev/null
+++ b/src/statistics/StatisticsParams.cpp
@@ -0,0 +1,2 @@
+#include "StatisticsParams.h"
+#include "../dictionary/CardPack.h"
diff --git a/src/statistics/StatisticsParams.h b/src/statistics/StatisticsParams.h
new file mode 100644
index 0000000..9a805c6
--- /dev/null
+++ b/src/statistics/StatisticsParams.h
@@ -0,0 +1,20 @@
+#ifndef STATISTICS_PARAMS_H
+#define STATISTICS_PARAMS_H
+
+class CardPack;
+
+class StatisticsParams
+{
+public:
+ static const int Week = 7;
+
+public:
+ const CardPack* getCardPack() const { return cardPack; }
+ int getTimePeriod() const { return timePeriod; }
+
+protected:
+ const CardPack* cardPack;
+ int timePeriod;
+};
+
+#endif
diff --git a/src/statistics/StatisticsView.cpp b/src/statistics/StatisticsView.cpp
new file mode 100644
index 0000000..de1b16d
--- /dev/null
+++ b/src/statistics/StatisticsView.cpp
@@ -0,0 +1,182 @@
+#include "StatisticsView.h"
+#include "StudiedPage.h"
+#include "ScheduledPage.h"
+#include "ProgressPage.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/CardPack.h"
+
+const QSize StatisticsView::GridSize(150, 110);
+
+StatisticsView::StatisticsView(const Dictionary* dict):
+ dict(dict), periodLabel(NULL), periodBox(NULL)
+{
+ init();
+ createContentsList();
+ createPages();
+ createListItems();
+ createUi();
+ loadSettings();
+}
+
+void StatisticsView::init()
+{
+ cardPack = dict->cardPacks().first();
+ timePeriod = Week;
+ setWindowTitle(tr("Statistics") + " - " + dict->shortName());
+ setWindowIcon(QIcon(":/images/statistics.png"));
+}
+
+void StatisticsView::closeEvent(QCloseEvent* /*event*/)
+{
+ saveSettings();
+}
+
+void StatisticsView::createPages()
+{
+ pagesWidget = new QStackedWidget;
+ pagesWidget->addWidget(new ProgressPage(this));
+ pagesWidget->addWidget(new StudiedPage(this));
+ pagesWidget->addWidget(new ScheduledPage(this));
+}
+
+void StatisticsView::createContentsList()
+{
+ contentsWidget = new QListWidget;
+ contentsWidget->setViewMode(QListView::IconMode);
+ contentsWidget->setGridSize(GridSize);
+ contentsWidget->setIconSize(QSize(IconSize, IconSize));
+ contentsWidget->setMovement(QListView::Static);
+ contentsWidget->setFixedWidth(GridSize.width() + 4);
+}
+
+void StatisticsView::createListItems()
+{
+ QStringList icons = {"pie-chart-3d", "chart-past", "chart-future"};
+ for(int i = 0; i < icons.size(); i++)
+ new QListWidgetItem( QIcon(QString(":/images/%1.png").arg(icons[i])),
+ static_cast<BaseStatPage*>(pagesWidget->widget(i))->getTitle(),
+ contentsWidget);
+ connect(contentsWidget,
+ SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)),
+ SLOT(changePage(QListWidgetItem*, QListWidgetItem*)));
+ contentsWidget->setCurrentRow(0);
+}
+
+void StatisticsView::changePage(QListWidgetItem* curPage, QListWidgetItem* prevPage)
+{
+ if(!curPage)
+ curPage = prevPage;
+ pagesWidget->setCurrentIndex(contentsWidget->row(curPage));
+ updateChart();
+ updatePeriodBox();
+}
+
+void StatisticsView::createUi()
+{
+ QVBoxLayout* verLt = new QVBoxLayout;
+ verLt->addLayout(createControlLayout());
+ verLt->addWidget(pagesWidget);
+
+ QHBoxLayout* horLt = new QHBoxLayout;
+ horLt->addWidget(contentsWidget);
+ horLt->addLayout(verLt);
+ setLayout(horLt);
+}
+
+void StatisticsView::loadSettings()
+{
+ QSettings settings;
+ QVariant pointVar = settings.value("stats-pos");
+ if(pointVar.isNull())
+ return;
+ move(pointVar.toPoint());
+ resize(settings.value("stats-size").toSize());
+}
+
+void StatisticsView::saveSettings()
+{
+ QSettings settings;
+ settings.setValue("stats-pos", pos());
+ settings.setValue("stats-size", size());
+}
+
+QBoxLayout* StatisticsView::createControlLayout()
+{
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->addWidget(new QLabel(tr("Card pack:")));
+ lt->addWidget(createPacksBox());
+ lt->addStretch();
+ periodLabel = new QLabel(tr("Period:"));
+ lt->addWidget(periodLabel);
+ lt->addWidget(createPeriodBox());
+ return lt;
+}
+
+QComboBox* StatisticsView::createPacksBox()
+{
+ QComboBox* packsBox = new QComboBox;
+ foreach(CardPack* pack, dict->cardPacks())
+ packsBox->addItem(pack->id());
+ connect(packsBox, SIGNAL(activated(int)), SLOT(setPack(int)));
+ return packsBox;
+}
+
+QComboBox* StatisticsView::createPeriodBox()
+{
+ periodBox = new QComboBox;
+ QPair<QString, int> period;
+ foreach(period, getPeriodsList())
+ periodBox->addItem(period.first, period.second);
+ periodBox->setMaxVisibleItems(12);
+ connect(periodBox, SIGNAL(activated(int)), SLOT(setPeriod(int)));
+ updatePeriodBox();
+ return periodBox;
+}
+
+QList<QPair<QString, int>> StatisticsView::getPeriodsList()
+{
+ static const int Week = 7;
+ static const int Month = 30;
+ static const int Year = 365;
+ return {
+ qMakePair(tr("%n week(s)", 0, 1), Week),
+ qMakePair(tr("%n week(s)", 0, 2), 2 * Week),
+ qMakePair(tr("%n week(s)", 0, 4), 4 * Week),
+ qMakePair(tr("%n month(s)", 0, 1), Month + 1),
+ qMakePair(tr("%n month(s)", 0, 2), 2 * Month + 1),
+ qMakePair(tr("%n month(s)", 0, 3), 3 * Month + 2),
+ qMakePair(tr("%n month(s)", 0, 6), 6 * Month + 3),
+ qMakePair(tr("%n year(s)", 0, 1), Year),
+ qMakePair(tr("%n year(s)", 0, 2), 2 * Year),
+ qMakePair(tr("%n year(s)", 0, 3), 3 * Year),
+ qMakePair(tr("All time"), -1)};
+}
+
+void StatisticsView::setPack(int packIndex)
+{
+ cardPack = dict->cardPacks().at(packIndex);
+ updateChart();
+}
+
+void StatisticsView::setPeriod(int index)
+{
+ timePeriod = periodBox->itemData(index).toInt();
+ updateChart();
+}
+
+void StatisticsView::updateChart()
+{
+ static_cast<BaseStatPage*>(pagesWidget->currentWidget())->updateDataSet();
+}
+
+void StatisticsView::updatePeriodBox()
+{
+ bool visiblePeriod = static_cast<BaseStatPage*>(
+ pagesWidget->currentWidget())->usesTimePeriod();
+ if(periodBox)
+ {
+ periodLabel->setVisible(visiblePeriod);
+ periodBox->setVisible(visiblePeriod);
+ }
+}
+
diff --git a/src/statistics/StatisticsView.h b/src/statistics/StatisticsView.h
new file mode 100644
index 0000000..560f8b8
--- /dev/null
+++ b/src/statistics/StatisticsView.h
@@ -0,0 +1,53 @@
+#ifndef STATISTICS_VIEW_H
+#define STATISTICS_VIEW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "StatisticsParams.h"
+
+class Dictionary;
+class CardPack;
+
+class StatisticsView: public QDialog, public StatisticsParams
+{
+ Q_OBJECT
+public:
+ StatisticsView(const Dictionary* dict);
+
+private:
+ static QList<QPair<QString, int>> getPeriodsList();
+
+private:
+ void init();
+ void closeEvent(QCloseEvent *event);
+ void createPages();
+ void createContentsList();
+ void createListItems();
+ void createUi();
+ void loadSettings();
+ void saveSettings();
+ QBoxLayout* createControlLayout();
+ QComboBox* createPacksBox();
+ QComboBox* createPeriodBox();
+ void updateChart();
+ void updatePeriodBox();
+
+private slots:
+ void changePage(QListWidgetItem* curPage, QListWidgetItem* prevPage);
+ void setPack(int packIndex);
+ void setPeriod(int index);
+
+private:
+ static const QSize GridSize;
+ static const int IconSize = 75;
+
+private:
+ const Dictionary* dict;
+ QListWidget* contentsWidget;
+ QLabel* periodLabel;
+ QComboBox* periodBox;
+ QStackedWidget* pagesWidget;
+};
+
+#endif
diff --git a/src/statistics/StudiedPage.cpp b/src/statistics/StudiedPage.cpp
new file mode 100644
index 0000000..5dbe83b
--- /dev/null
+++ b/src/statistics/StudiedPage.cpp
@@ -0,0 +1,17 @@
+#include "StudiedPage.h"
+#include "../dictionary/CardPack.h"
+#include "../study/StudyRecord.h"
+
+StudiedPage::StudiedPage(const StatisticsParams* statParams):
+ TimeChartPage(statParams)
+{
+ init();
+}
+
+QList<QDateTime> StudiedPage::getDates(const CardPack* pack) const
+{
+ QList<QDateTime> res;
+ foreach(StudyRecord record, pack->getStudyRecords())
+ res << record.date;
+ return res;
+}
diff --git a/src/statistics/StudiedPage.h b/src/statistics/StudiedPage.h
new file mode 100644
index 0000000..5ccb59b
--- /dev/null
+++ b/src/statistics/StudiedPage.h
@@ -0,0 +1,18 @@
+#ifndef STUDIED_PAGE_H
+#define STUDIED_PAGE_H
+
+#include "TimeChartPage.h"
+
+class StudiedPage: public TimeChartPage
+{
+ Q_OBJECT
+public:
+ StudiedPage(const StatisticsParams* statParams);
+ QString getTitle() const { return tr("Studied cards"); }
+
+protected:
+ QList<QDateTime> getDates(const CardPack* pack) const;
+ int getDataDirection() const { return -1; }
+};
+
+#endif
diff --git a/src/statistics/TimeChartPage.cpp b/src/statistics/TimeChartPage.cpp
new file mode 100644
index 0000000..62ca63b
--- /dev/null
+++ b/src/statistics/TimeChartPage.cpp
@@ -0,0 +1,54 @@
+#include "TimeChartPage.h"
+#include "StatisticsParams.h"
+#include "../charts/TimeChart.h"
+#include "../dictionary/CardPack.h"
+
+TimeChartPage::TimeChartPage(const StatisticsParams* statParams):
+ BaseStatPage(statParams)
+{
+}
+
+QWidget* TimeChartPage::createChart()
+{
+ chart = new TimeChart;
+ chart->setLabels(tr("Date"), tr("Cards"));
+ return chart;
+}
+
+void TimeChartPage::updateDataSet()
+{
+ QList<QDateTime> dates = getDates(statParams->getCardPack());
+ int period = statParams->getTimePeriod();
+ if(period == -1)
+ period = getStudyPeriodLength(dates);
+ int reviewsNum = getReviewsNum(dates, period);
+ totalReviewsLabel->setText(tr("Total: %1").arg(reviewsNum));
+ chart->setDates(dates, period, getDataDirection());
+}
+
+int TimeChartPage::getStudyPeriodLength(const QList<QDateTime>& dates) const
+{
+ const int minPeriod = 7;
+ QDateTime minDate = QDateTime::currentDateTime();
+ QDateTime maxDate = minDate;
+ foreach(QDateTime date, dates)
+ if(date < minDate)
+ minDate = date;
+ else if(date > maxDate)
+ maxDate = date;
+ int res = minDate.daysTo(maxDate) + 1;
+ if(res < minPeriod)
+ res = minPeriod;
+ return res;
+}
+
+int TimeChartPage::getReviewsNum(const QList<QDateTime>& dates,
+ int period) const
+{
+ QDateTime curDate = QDateTime::currentDateTime();
+ int res = 0;
+ foreach(QDateTime date, dates)
+ if(qAbs(date.daysTo(curDate)) < period)
+ res++;
+ return res;
+}
diff --git a/src/statistics/TimeChartPage.h b/src/statistics/TimeChartPage.h
new file mode 100644
index 0000000..5e48d4c
--- /dev/null
+++ b/src/statistics/TimeChartPage.h
@@ -0,0 +1,33 @@
+#ifndef TIME_CHART_PAGE_H
+#define TIME_CHART_PAGE_H
+
+#include <QtWidgets>
+#include "BaseStatPage.h"
+#include "../charts/DataPoint.h"
+
+class TimeChart;
+class StatisticsParams;
+class CardPack;
+
+class TimeChartPage: public BaseStatPage
+{
+ Q_OBJECT
+public:
+ TimeChartPage(const StatisticsParams* statParams);
+ void updateDataSet();
+ bool usesTimePeriod() const {return true;}
+
+protected:
+ virtual QList<QDateTime> getDates(const CardPack* pack) const = 0;
+ virtual int getDataDirection() const = 0;
+ QWidget* createChart();
+
+private:
+ int getStudyPeriodLength(const QList<QDateTime>& dates) const;
+ int getReviewsNum(const QList<QDateTime>& dates, int period) const;
+
+private:
+ TimeChart* chart;
+};
+
+#endif
diff --git a/src/strings.cpp b/src/strings.cpp
new file mode 100644
index 0000000..54d2fb9
--- /dev/null
+++ b/src/strings.cpp
@@ -0,0 +1,6 @@
+#include "strings.h"
+
+const char* Strings::s_build = QT_TRANSLATE_NOOP("Strings", "Build");
+const char* Strings::s_author = QT_TRANSLATE_NOOP("Strings", "Author: Mykhaylo Kopytonenko");
+const char* Strings::s_appTitle = QT_TRANSLATE_NOOP("Strings", "Fresh Memory");
+const char* Strings::s_error = QT_TRANSLATE_NOOP("Strings", "Error");
diff --git a/src/strings.h b/src/strings.h
new file mode 100644
index 0000000..bd7925a
--- /dev/null
+++ b/src/strings.h
@@ -0,0 +1,22 @@
+#ifndef STRINGS_H
+#define STRINGS_H
+
+#include <QObject>
+#include <QString>
+
+class Strings: public QObject
+{
+ Q_OBJECT
+
+public:
+ static const char* s_build;
+ static const char* s_author;
+ static const char* s_appTitle;
+ static const char* s_error;
+
+public:
+ static QString errorTitle() { return tr(s_appTitle) + " - " + tr(s_error); }
+
+};
+
+#endif
diff --git a/src/study/CardEditDialog.cpp b/src/study/CardEditDialog.cpp
new file mode 100644
index 0000000..5dc47a3
--- /dev/null
+++ b/src/study/CardEditDialog.cpp
@@ -0,0 +1,119 @@
+#include "CardEditDialog.h"
+#include "IStudyWindow.h"
+#include "IStudyModel.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/Dictionary.h"
+#include "../main-view/DictTableModel.h"
+#include "../main-view/DictTableView.h"
+#include "../main-view/CardFilterModel.h"
+#include "../main-view/MainWindow.h"
+
+#include <QSettings>
+#include <QCloseEvent>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QVariant>
+
+CardEditDialog::CardEditDialog(Card* aCurCard, MainWindow* aMainWindow, IStudyWindow* aStudyWindow ):
+ QDialog( aStudyWindow ), m_cardEditModel( NULL ), m_cardEditView( NULL ), m_mainWindow( aMainWindow )
+ {
+ DictTableModel* tableModel = aStudyWindow->studyModel()->getDictModel();
+ if( !tableModel )
+ return;
+
+ m_cardEditModel = new CardFilterModel( tableModel );
+ m_cardEditModel->setSourceModel( tableModel );
+
+ const CardPack* cardPack = static_cast<const CardPack*>(aCurCard->getCardPack());
+ Q_ASSERT(cardPack);
+ m_dictionary = static_cast<const Dictionary*>(cardPack->dictionary());
+ Q_ASSERT( m_dictionary );
+ foreach( const DicRecord* record, aCurCard->getSourceRecords() )
+ {
+ int row = m_dictionary->indexOfRecord( const_cast<DicRecord*>( record ) );
+ if( row > -1 )
+ m_cardEditModel->addFilterRow( row );
+ }
+
+ m_cardEditView = new DictTableView( m_cardEditModel, this );
+ m_cardEditView->setContextMenuPolicy( Qt::ActionsContextMenu );
+ m_cardEditView->verticalHeader()->setContextMenuPolicy( Qt::ActionsContextMenu );
+ if( m_mainWindow )
+ {
+ m_cardEditView->addActions(m_mainWindow->getContextMenuActions());
+ m_cardEditView->verticalHeader()->addActions(m_mainWindow->getContextMenuActions());
+ connect( m_cardEditView->selectionModel(),
+ SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
+ m_mainWindow, SLOT(updateSelectionActions()) );
+ }
+
+ QPushButton* dicWindowBtn = new QPushButton(tr("Go to dictionary window"));
+ connect( dicWindowBtn, SIGNAL(clicked()), SLOT(goToDictionaryWindow()));
+ QPushButton* closeBtn = new QPushButton(tr("Close"));
+ connect( closeBtn, SIGNAL(clicked()), SLOT(close()));
+
+ QVBoxLayout* editLt = new QVBoxLayout;
+ editLt->addWidget( m_cardEditView );
+ QHBoxLayout* buttonsLt = new QHBoxLayout;
+ buttonsLt->addStretch( 1 );
+ buttonsLt->addWidget( dicWindowBtn );
+ buttonsLt->addWidget( closeBtn );
+ editLt->addLayout( buttonsLt );
+
+ setLayout( editLt );
+ setWindowTitle( QString(tr("Edit card: ", "In title of card edit view")) + aCurCard->getName() );
+ setWindowModality( Qt::WindowModal );
+ closeBtn->setFocus();
+ m_cardEditView->setCurrentIndex( m_cardEditModel->index(0, 0) );
+
+ QSettings settings;
+ QVariant posVar = settings.value("cardeditview-pos");
+ if( !posVar.isNull() )
+ move( posVar.toPoint() );
+ resize( settings.value("cardeditview-size", QSize(CardEditViewWidth, CardEditViewHeight)).toSize() );
+ }
+
+CardEditDialog::~CardEditDialog()
+ {
+ delete m_cardEditView;
+ delete m_cardEditModel;
+ }
+
+void CardEditDialog::closeEvent( QCloseEvent* event )
+ {
+ QSettings settings;
+ settings.setValue("cardeditview-pos", pos());
+ settings.setValue("cardeditview-size", size());
+ event->accept();
+ }
+
+bool CardEditDialog::event( QEvent* event )
+ {
+ if( event->type() == QEvent::WindowActivate || event->type() == QEvent::WindowDeactivate )
+ {
+ m_mainWindow->updateSelectionActions();
+ return true;
+ }
+ return QDialog::event( event );
+ }
+
+const DictTableView* CardEditDialog::cardEditView() const
+ {
+ if( isActiveWindow() )
+ return m_cardEditView;
+ else
+ return NULL;
+ }
+
+void CardEditDialog::goToDictionaryWindow()
+ {
+ close();
+ QModelIndex curProxyIndex = m_cardEditView->currentIndex();
+ QModelIndex curSourceIndex = m_cardEditModel->mapToSource( curProxyIndex );
+ if( !m_dictionary )
+ return;
+ m_mainWindow->goToDictionaryRecord( m_dictionary, curSourceIndex.row() );
+ }
diff --git a/src/study/CardEditDialog.h b/src/study/CardEditDialog.h
new file mode 100644
index 0000000..3eb3d1b
--- /dev/null
+++ b/src/study/CardEditDialog.h
@@ -0,0 +1,42 @@
+#ifndef CARDEDITDIALOG_H
+#define CARDEDITDIALOG_H
+
+#include <QDialog>
+#include <QEvent>
+
+class Card;
+class Dictionary;
+class CardFilterModel;
+class DictTableView;
+class DictTableModel;
+class MainWindow;
+class IStudyWindow;
+
+class CardEditDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ CardEditDialog(Card* aCurCard, MainWindow* aMainWindow, IStudyWindow* aStudyWindow);
+ ~CardEditDialog();
+
+ const DictTableView* cardEditView() const;
+
+protected:
+ void closeEvent( QCloseEvent* event );
+ bool event( QEvent* event );
+
+private slots:
+ void goToDictionaryWindow();
+
+private:
+ static const int CardEditViewHeight = 130;
+ static const int CardEditViewWidth = 600;
+
+ const Dictionary* m_dictionary;
+ CardFilterModel* m_cardEditModel;
+ DictTableView* m_cardEditView;
+ MainWindow* m_mainWindow;
+};
+
+#endif
diff --git a/src/study/CardSideView.cpp b/src/study/CardSideView.cpp
new file mode 100644
index 0000000..8956210
--- /dev/null
+++ b/src/study/CardSideView.cpp
@@ -0,0 +1,205 @@
+#include "CardSideView.h"
+#include "../field-styles/FieldStyleFactory.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/IDictionary.h"
+
+#include <QtCore>
+
+CardSideView::CardSideView( bool aMode ):
+ m_showMode(aMode), cardPack(NULL)
+{
+ setFrameStyle( QFrame::Plain | QFrame::Box );
+ setAlignment( Qt::AlignCenter );
+ setTextFormat( Qt::RichText );
+ setWordWrap( true );
+ setAutoFillBackground( true );
+ setPalette( FieldStyleFactory::inst()->cardBgColor );
+ newIcon = new QLabel(this);
+ newIcon->setPixmap(QPixmap(":/images/new-topright.png"));
+ updateNewIconPos();
+ newIcon->hide();
+}
+
+void CardSideView::setEnabled( bool aEnabled )
+ {
+ QLabel::setEnabled( aEnabled );
+ setBackgroundVisible( aEnabled );
+ }
+
+/** Cleans background and text */
+void CardSideView::setBackgroundVisible( bool aVisible )
+ {
+ if( aVisible )
+ QLabel::setEnabled( true );
+ setAutoFillBackground( aVisible );
+ setLineWidth( aVisible? 1 : 0 );
+ if( !aVisible )
+ setText("");
+ }
+
+void CardSideView::setPack(const CardPack* pack)
+ {
+ cardPack = pack;
+ }
+
+void CardSideView::setQstAnsr( const QString aQuestion, const QStringList aAnswers )
+ {
+ m_question = aQuestion;
+ m_answers = aAnswers;
+ updateText();
+ }
+
+void CardSideView::setQuestion( const QString aQuestion )
+ {
+ m_question = aQuestion;
+ updateText();
+ }
+
+void CardSideView::setShowMode( bool aMode )
+ {
+ m_showMode = aMode;
+ updateText();
+ }
+
+void CardSideView::updateText()
+{
+ setText(getFormattedText());
+}
+
+QString CardSideView::getFormattedText() const
+{
+ if(!cardPack)
+ return QString();
+ if(m_showMode == QstMode)
+ return getFormattedQuestion();
+ else
+ return getFormattedAnswer();
+}
+
+QString CardSideView::getFormattedQuestion() const
+{
+ const Field* qstField = cardPack->getQuestionField();
+ if(!qstField)
+ return QString();
+ return formattedField(m_question, qstField->style());
+}
+
+QString CardSideView::getFormattedAnswer() const
+{
+ QStringList formattedAnswers;
+ int i = 0;
+ foreach(const Field* field, cardPack->getAnswerFields())
+ {
+ QString text = getFormattedAnswerField(field, i);
+ if(i == 0)
+ text += "<br/>";
+ if(!text.isEmpty())
+ formattedAnswers << text;
+ i++;
+ }
+ return formattedAnswers.join("<br/>");
+}
+
+QString CardSideView::getFormattedAnswerField(const Field* field, int index) const
+{
+ if(!field)
+ return QString();
+ if(index >= m_answers.size())
+ return QString();
+ if(m_answers[index].isEmpty())
+ return QString();
+ return formattedField(m_answers[index], field->style() );
+}
+
+QString CardSideView::formattedField( const QString aField, const QString aStyle ) const
+ {
+ Q_ASSERT( cardPack );
+ QString text = static_cast<IDictionary*>(cardPack->dictionary())->
+ extendImagePaths( aField );
+
+ FieldStyle fieldStyle = FieldStyleFactory::inst()->getStyle( aStyle );
+ QString beginning("<span style=\"");
+ beginning += QString("font-family:'%1'").arg( fieldStyle.font.family() );
+ beginning += QString("; font-size:%1pt").arg( fieldStyle.font.pointSize() );
+ beginning += getHighlighting(fieldStyle);
+ beginning += "\">" + fieldStyle.prefix;
+ QString ending( fieldStyle.suffix + "</span>");
+
+ // Highlight keywords inside plain text. Ignore tags.
+ if(m_showMode == AnsMode && fieldStyle.hasKeyword)
+ {
+ QString highlightedText;
+ int curPos = 0;
+ while( curPos < text.length() )
+ {
+ QString curText;
+ int beginTagPos = text.indexOf( "<", curPos );
+ if( beginTagPos > -1 )
+ curText = text.mid( curPos, beginTagPos - curPos ); // copy plain text
+ else
+ curText = text.mid( curPos, -1 ); // copy until end of string
+ curText = highlightKeyword(curText, fieldStyle);
+ highlightedText += curText;
+ if( beginTagPos == -1 )
+ break;
+ int endTagPos = text.indexOf( ">", beginTagPos );
+ if( endTagPos > -1 )
+ highlightedText += text.mid( beginTagPos, endTagPos - beginTagPos + 1 ); // copy tag
+ else
+ {
+ highlightedText += text.mid( curPos, -1 ); // copy until end of string
+ break;
+ }
+ curPos = endTagPos + 1;
+ }
+ text = highlightedText;
+ }
+
+ return beginning + text + ending;
+ }
+
+QString CardSideView::highlightKeyword( const QString aText, const FieldStyle& fieldStyle ) const
+ {
+ QString resText = aText;
+ if( !m_question.isEmpty() )
+ resText.replace(QRegExp( "\\b(\\w*" + QRegExp::escape( m_question ) + "\\w*)\\b",
+ Qt::CaseInsensitive ), "[\\1]");
+ QString spanBegin("<span style=\"");
+ spanBegin += getHighlighting(fieldStyle.getKeywordStyle()) + "\">";
+ resText.replace('[', spanBegin);
+ resText.replace( ']', "</span>" );
+ return resText;
+ }
+
+QString CardSideView::getHighlighting(const FieldStyle& fieldStyle) const
+{
+ QString res;
+ if(fieldStyle.color != Qt::black)
+ res += QString("; color:%1").arg(fieldStyle.color.name());
+ if(fieldStyle.font.bold())
+ res += "; font-weight:bold";
+ if(fieldStyle.font.italic())
+ res += "; font-style:italic";
+ return res;
+}
+
+QSize CardSideView::sizeHint() const
+{
+ return QSize(300, 200);
+}
+
+void CardSideView::updateNewIconPos()
+{
+ newIcon->move(rect().topRight() - QPoint(newIcon->width(), 0));
+}
+
+void CardSideView::showNewIcon(bool visible)
+{
+ newIcon->setVisible(visible);
+ updateNewIconPos();
+}
+
+void CardSideView::resizeEvent(QResizeEvent* /*event*/)
+{
+ updateNewIconPos();
+}
diff --git a/src/study/CardSideView.h b/src/study/CardSideView.h
new file mode 100644
index 0000000..c67ebe4
--- /dev/null
+++ b/src/study/CardSideView.h
@@ -0,0 +1,52 @@
+#ifndef CARDSIDEVIEW_H
+#define CARDSIDEVIEW_H
+
+#include <QLabel>
+
+class CardPack;
+class FieldStyle;
+class Field;
+
+class CardSideView : public QLabel
+{
+ Q_OBJECT
+public:
+ CardSideView( bool aMode = QstMode );
+ void setPack( const CardPack* pack );
+ void setQstAnsr( const QString aQuestion, const QStringList aAnswers );
+ void setQuestion( const QString aQuestion );
+ void setShowMode( bool aMode );
+ QSize sizeHint() const;
+ QString getFormattedText() const;
+ void showNewIcon(bool visible);
+
+public:
+ static const bool QstMode = true;
+ static const bool AnsMode = false;
+
+public slots:
+ void setEnabled( bool aEnabled );
+
+protected:
+ void resizeEvent(QResizeEvent* event);
+
+private:
+ void updateText();
+ void setBackgroundVisible( bool aVisible );
+ QString formattedField( const QString aField, const QString aStyle ) const;
+ QString highlightKeyword( const QString aText, const FieldStyle& fieldStyle ) const;
+ QString getHighlighting(const FieldStyle& fieldStyle) const;
+ QString getFormattedQuestion() const;
+ QString getFormattedAnswer() const;
+ QString getFormattedAnswerField(const Field* field, int index) const;
+ void updateNewIconPos();
+
+private:
+ bool m_showMode;
+ const CardPack* cardPack;
+ QString m_question;
+ QStringList m_answers;
+ QLabel* newIcon;
+};
+
+#endif
diff --git a/src/study/CardsStatusBar.cpp b/src/study/CardsStatusBar.cpp
new file mode 100644
index 0000000..e29f9f8
--- /dev/null
+++ b/src/study/CardsStatusBar.cpp
@@ -0,0 +1,82 @@
+#include "CardsStatusBar.h"
+
+const QStringList CardsStatusBar::Colors = {"green", "#fffd7c", "white", "#bd8d71"};
+
+CardsStatusBar::CardsStatusBar(QWidget* aParent) :
+ QWidget(aParent)
+{
+ setMinimumHeight(10);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+}
+
+CardsStatusBar::~CardsStatusBar()
+{
+ delete painter;
+}
+
+void CardsStatusBar::paintEvent(QPaintEvent* aEvent)
+{
+ QWidget::paintEvent( aEvent );
+ if(values.isEmpty())
+ return;
+ max = getMax();
+
+ painter = new QPainter(this);
+ painter->setRenderHint(QPainter::Antialiasing);
+ painter->save();
+
+ QPainterPath path;
+ path.addRoundedRect( rect(), Radius, Radius);
+ painter->setClipPath( path );
+
+ barRect = rect().adjusted(0, 0, 1, 0);
+
+ sectionLeft = rect().left();
+ for(int i = 0; i < values.size(); i++)
+ drawSection(i);
+
+ painter->restore();
+
+ painter->setPen(QPen("#7c7c7c"));
+ painter->drawRoundedRect(rect(), Radius, Radius);
+}
+
+int CardsStatusBar::getMax() const
+{
+ int max = 0;
+ foreach(int v, values)
+ max += v;
+ if(max == 0)
+ max = 1;
+ return max;
+}
+
+void CardsStatusBar::drawSection(int index)
+{
+ QRectF sectionRect = getSectionRect(index);
+ painter->setPen( Qt::NoPen );
+ painter->setBrush(getSectionGradient(index));
+ painter->drawRect(sectionRect);
+
+ painter->setPen(QPen(QBrush("#a7a7a7"), 0.5));
+ painter->drawLine(sectionRect.topRight(), sectionRect.bottomRight());
+
+ sectionLeft += sectionRect.width();
+}
+
+QLinearGradient CardsStatusBar::getSectionGradient(int index)
+{
+ QLinearGradient grad(barRect.topLeft(), barRect.bottomLeft());
+ grad.setColorAt( 0, Qt::white);
+ grad.setColorAt( 0.9, QColor(Colors[index]) );
+ return grad;
+}
+
+QRectF CardsStatusBar::getSectionRect(int index)
+{
+ QRectF sectionRect = barRect;
+ sectionRect.moveLeft(sectionLeft);
+ qreal sectionWidth = barRect.width() * values[index] / max;
+ sectionRect.setWidth(sectionWidth);
+ return sectionRect;
+}
diff --git a/src/study/CardsStatusBar.h b/src/study/CardsStatusBar.h
new file mode 100644
index 0000000..10344f7
--- /dev/null
+++ b/src/study/CardsStatusBar.h
@@ -0,0 +1,37 @@
+#ifndef CARDSSTATUSBAR_H
+#define CARDSSTATUSBAR_H
+
+#include <QtWidgets>
+
+class CardsStatusBar : public QWidget
+{
+ Q_OBJECT
+public:
+ static const QStringList Colors;
+
+public:
+ CardsStatusBar(QWidget* aParent = 0);
+ ~CardsStatusBar();
+ void setValues(const QList<int>& values) { this->values = values; update(); }
+
+protected:
+ virtual void paintEvent(QPaintEvent* aEvent);
+
+private:
+ int getMax() const;
+ void drawSection(int index);
+ QLinearGradient getSectionGradient(int index);
+ QRectF getSectionRect(int index);
+
+private:
+ static const int Radius = 6;
+
+private:
+ QList<int> values;
+ qreal max;
+ QRectF barRect;
+ QPainter* painter;
+ qreal sectionLeft;
+};
+
+#endif
diff --git a/src/study/IStudyModel.cpp b/src/study/IStudyModel.cpp
new file mode 100644
index 0000000..3194a71
--- /dev/null
+++ b/src/study/IStudyModel.cpp
@@ -0,0 +1,9 @@
+#include "IStudyModel.h"
+
+#include "../dictionary/CardPack.h"
+
+IStudyModel::IStudyModel( CardPack* aCardPack ):
+ cardPack(aCardPack), dictModel(NULL), curCardNum(-1)
+{
+}
+
diff --git a/src/study/IStudyModel.h b/src/study/IStudyModel.h
new file mode 100644
index 0000000..392ef01
--- /dev/null
+++ b/src/study/IStudyModel.h
@@ -0,0 +1,35 @@
+#ifndef ISTUDYMODEL_H
+#define ISTUDYMODEL_H
+
+#include <QObject>
+
+class Card;
+class CardPack;
+class DictTableModel;
+
+class IStudyModel: public QObject
+{
+Q_OBJECT
+
+public:
+ IStudyModel( CardPack* aCardPack );
+ virtual ~IStudyModel() {}
+
+public:
+ CardPack* getCardPack() { return cardPack; }
+ virtual Card* getCurCard() const = 0;
+ DictTableModel* getDictModel() const { return dictModel; }
+
+ void setDictModel( DictTableModel* aModel ) { dictModel = aModel; }
+
+signals:
+ void nextCardSelected();
+ void curCardUpdated();
+
+protected:
+ CardPack* cardPack;
+ DictTableModel* dictModel;
+ int curCardNum; ///< Number of the current card in this session, base=0.
+};
+
+#endif
diff --git a/src/study/IStudyWindow.cpp b/src/study/IStudyWindow.cpp
new file mode 100644
index 0000000..f918936
--- /dev/null
+++ b/src/study/IStudyWindow.cpp
@@ -0,0 +1,293 @@
+#include "IStudyWindow.h"
+
+#include <QCloseEvent>
+#include <QMessageBox>
+
+#include "IStudyModel.h"
+#include "CardSideView.h"
+#include "CardEditDialog.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/CardPack.h"
+#include "../main-view/MainWindow.h"
+
+const int IStudyWindow::AnsButtonPage = 0;
+const int IStudyWindow::AnsLabelPage = 1;
+const int IStudyWindow::CardPage = 0;
+const int IStudyWindow::MessagePage = 1;
+
+IStudyWindow::IStudyWindow(IStudyModel* aModel , QString aStudyName, QWidget *aParent):
+ m_model( aModel ), curCard( NULL ), state(StateAnswerHidden), m_studyName( aStudyName ),
+ m_parentWidget( aParent ), m_cardEditDialog( NULL )
+{
+ setMinimumWidth(MinWidth);
+ setMaximumWidth(MaxWidth);
+ setAttribute( Qt::WA_DeleteOnClose );
+ connect( m_model, SIGNAL(nextCardSelected()), SLOT(showNextCard()) );
+ connect( m_model, SIGNAL(curCardUpdated()), SLOT(updateCurCard()) );
+}
+
+IStudyWindow::~IStudyWindow()
+{
+ delete m_model;
+}
+
+void IStudyWindow::OnDictionaryRemoved()
+{
+ close();
+}
+
+void IStudyWindow::createUI()
+{
+ centralStackedLt = new QStackedLayout;
+ centralStackedLt->addWidget(createWrapper(createCardView()));
+ centralStackedLt->addWidget(createWrapper(createMessageLayout()));
+ centralStackedLt->setCurrentIndex(CardPage);
+ connect(centralStackedLt, SIGNAL(currentChanged(int)), SLOT(updateToolBarVisibility(int)));
+
+ QVBoxLayout* mainLayout = new QVBoxLayout;
+ mainLayout->addLayout(createUpperPanel());
+ mainLayout->addLayout(centralStackedLt);
+ mainLayout->addLayout(createLowerPanel());
+ setLayout(mainLayout);
+
+ ReadSettings();
+}
+
+QWidget* IStudyWindow::createWrapper(QBoxLayout* layout)
+{
+ QWidget* widget = new QWidget;
+ widget->setLayout(layout);
+ return widget;
+}
+
+QVBoxLayout* IStudyWindow::createMessageLayout()
+{
+ messageLabel = createMessageLabel();
+
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->setContentsMargins(QMargins());
+ lt->addStretch();
+ lt->addWidget(messageLabel);
+ lt->addStretch();
+ lt->addWidget(createClosePackButton(), 0, Qt::AlignHCenter);
+ lt->addStretch();
+ return lt;
+}
+
+QLabel* IStudyWindow::createMessageLabel()
+{
+ QLabel* label = new QLabel;
+ label->setAutoFillBackground(true);
+ label->setPalette(QPalette("#fafafa"));
+ label->setWordWrap(true);
+ label->setFrameStyle(QFrame::StyledPanel);
+ label->setMinimumHeight(200);
+ return label;
+}
+
+QPushButton* IStudyWindow::createClosePackButton()
+{
+ QPushButton* button = new QPushButton(tr("Close this pack"));
+ button->setFixedSize(BigButtonWidth, BigButtonHeight);
+ connect(button, SIGNAL(clicked()), SLOT(close()));
+ return button;
+}
+
+QHBoxLayout* IStudyWindow::createUpperPanel()
+{
+ editCardBtn = createEditCardButton();
+ deleteCardBtn = createDeleteCardButton();
+
+ QHBoxLayout* upperPanelLt = new QHBoxLayout;
+ upperPanelLt->addWidget(new QLabel(m_model->getCardPack()->id()));
+ upperPanelLt->addWidget(deleteCardBtn);
+ upperPanelLt->addWidget(editCardBtn);
+
+ return upperPanelLt;
+}
+
+QToolButton* IStudyWindow::createEditCardButton()
+{
+ QToolButton* button = new QToolButton( this );
+ button->setIcon( QIcon(":/images/pencil.png") );
+ button->setIconSize( QSize(ToolBarIconSize, ToolBarIconSize) );
+ button->setShortcut( tr("E", "Shortcut for 'Edit card' button") );
+ button->setToolTip( tr("Edit card")+" ("+button->shortcut().toString()+")" );
+ button->setFixedSize( ToolBarButtonSize, ToolBarButtonSize );
+ connect( button, SIGNAL(clicked()), SLOT(openCardEditDialog()) );
+ return button;
+}
+
+QToolButton* IStudyWindow::createDeleteCardButton()
+{
+ QToolButton* button = new QToolButton( this );
+ button->setIcon( QIcon(":/images/red-cross.png") );
+ button->setIconSize( QSize(ToolBarIconSize, ToolBarIconSize) );
+ button->setShortcut( tr("D", "Shortcut for 'Delete card' button") );
+ button->setToolTip( tr("Delete card")+" ("+button->shortcut().toString()+")" );
+ button->setFixedSize( ToolBarButtonSize, ToolBarButtonSize );
+ connect( button, SIGNAL(clicked()), SLOT(deleteCard()) );
+ return button;
+}
+
+QVBoxLayout* IStudyWindow::createCardView()
+{
+ questionLabel = new CardSideView( CardSideView::QstMode );
+ questionLabel->setPack( m_model->getCardPack() );
+
+ answerStackedLt = new QStackedLayout;
+ answerStackedLt->addWidget(createWrapper(createAnswerButtonLayout()));
+ answerStackedLt->addWidget(createWrapper(createAnswerLayout()));
+ answerStackedLt->setCurrentIndex(AnsButtonPage);
+
+ QVBoxLayout* cardViewLt = new QVBoxLayout;
+ cardViewLt->addWidget(questionLabel, 1);
+ cardViewLt->addLayout(answerStackedLt, 1);
+
+ return cardViewLt;
+}
+
+QBoxLayout* IStudyWindow::createAnswerButtonLayout()
+{
+ answerBtn = createAnswerButton();
+
+ QBoxLayout* lt = new QVBoxLayout;
+ lt->addStretch();
+ if(getAnswerEdit())
+ lt->addWidget(getAnswerEdit(), 0, Qt::AlignCenter);
+ lt->addWidget(answerBtn, 0, Qt::AlignCenter);
+ lt->addStretch();
+ return lt;
+}
+
+QPushButton* IStudyWindow::createAnswerButton()
+{
+ QPushButton* button = new QPushButton(QIcon(":/images/info.png"), tr("Show answer"));
+ button->setFixedSize(BigButtonWidth, BigButtonHeight);
+ button->setShortcut(Qt::Key_Return);
+ button->setToolTip(tr("Show answer") + ShortcutToStr(button) );
+ connect(button, SIGNAL(clicked()), SLOT(showAnswer()));
+ return button;
+}
+
+QBoxLayout* IStudyWindow::createAnswerLayout()
+{
+ answerLabel = new CardSideView( CardSideView::AnsMode );
+ answerLabel->setPack( m_model->getCardPack() );
+
+ QBoxLayout* lt = new QVBoxLayout;
+ lt->setContentsMargins(QMargins());
+ if(getUserAnswerLabel())
+ lt->addWidget(getUserAnswerLabel(), 1);
+ lt->addWidget(answerLabel, 1);
+ return lt;
+}
+
+const DictTableView* IStudyWindow::cardEditView() const
+ {
+ if( m_cardEditDialog )
+ return m_cardEditDialog->cardEditView();
+ else
+ return NULL;
+ }
+
+void IStudyWindow::openCardEditDialog()
+ {
+ if( m_cardEditDialog ) // already open
+ return;
+ Card* curCard = m_model->getCurCard();
+ Q_ASSERT( curCard );
+ MainWindow* mainWindow = dynamic_cast<MainWindow*>( m_parentWidget );
+ if( !mainWindow )
+ return;
+ m_cardEditDialog = new CardEditDialog( curCard, mainWindow, this );
+ m_cardEditDialog->exec();
+ delete m_cardEditDialog;
+ m_cardEditDialog = NULL;
+ Dictionary* dict = static_cast<Dictionary*>(m_model->getCardPack()->dictionary());
+ dict->save();
+ setWindowTitle(m_studyName + " - " + dict->shortName());
+ }
+
+void IStudyWindow::deleteCard()
+ {
+ QString question = m_model->getCurCard()->getQuestion();
+ QMessageBox::StandardButton pressedButton;
+ pressedButton = QMessageBox::question( this, tr("Delete card?"), tr("Delete card \"%1\"?").arg( question ),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes );
+ if( pressedButton == QMessageBox::Yes )
+ static_cast<Dictionary*>(m_model->getCardPack()->dictionary())->
+ removeRecord( question );
+ }
+
+void IStudyWindow::showNextCard()
+{
+ Q_ASSERT( m_model );
+ if( curCard )
+ disconnect( curCard, 0, this, 0 );
+ curCard = m_model->getCurCard();
+ if( curCard )
+ {
+ connect( curCard, SIGNAL(answersChanged()), SLOT(updateCurCard()) );
+ connect( curCard, SIGNAL(destroyed()), SLOT(invalidateCurCard()) );
+ }
+ setWindowTitle( m_studyName + " - " +
+ static_cast<Dictionary*>(m_model->getCardPack()->dictionary())->shortName() );
+ setStateForNextCard();
+ processState();
+}
+
+void IStudyWindow::setStateForNextCard()
+ {
+ if( curCard )
+ state = StateAnswerHidden;
+ else
+ state = StateNoCards;
+ }
+
+void IStudyWindow::showAnswer()
+ {
+ answerStackedLt->setCurrentIndex( AnsLabelPage );
+ if( state == StateAnswerHidden )
+ state = StateAnswerVisible;
+ else
+ return;
+ processState();
+ }
+
+void IStudyWindow::updateCurCard()
+{
+ if( curCard )
+ disconnect( curCard, 0, this, 0 );
+ curCard = m_model->getCurCard();
+ if( curCard )
+ {
+ connect( curCard, SIGNAL(answersChanged()), SLOT(updateCurCard()) );
+ connect( curCard, SIGNAL(destroyed()), SLOT(invalidateCurCard()) );
+ }
+ setWindowTitle( m_studyName + " - " +
+ static_cast<Dictionary*>(m_model->getCardPack()->dictionary())->shortName() );
+ int origState = state;
+ if( state == StateAnswerHidden || state == StateAnswerVisible )
+ {
+ state = StateAnswerHidden;
+ processState();
+ }
+ if( origState == StateAnswerVisible )
+ {
+ state = StateAnswerVisible;
+ processState();
+ }
+}
+
+QString IStudyWindow::ShortcutToStr( QAbstractButton* aButton )
+{
+ return " (" + aButton->shortcut().toString( QKeySequence::NativeText ) + ")";
+}
+
+void IStudyWindow::updateToolBarVisibility(int index)
+{
+ bool visible = (index == CardPage);
+ deleteCardBtn->setVisible(visible);
+ editCardBtn->setVisible(visible);
+}
diff --git a/src/study/IStudyWindow.h b/src/study/IStudyWindow.h
new file mode 100644
index 0000000..1f0bd0d
--- /dev/null
+++ b/src/study/IStudyWindow.h
@@ -0,0 +1,107 @@
+#ifndef ISTUDYWINDOW_H
+#define ISTUDYWINDOW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "../main-view/AppModel.h"
+
+class IStudyModel;
+class Card;
+class Dictionary;
+class QProgressBar;
+class CardSideView;
+class DictTableView;
+class CardEditDialog;
+
+class IStudyWindow: public QWidget
+{
+ Q_OBJECT
+
+protected:
+ enum
+ {
+ StateAnswerHidden,
+ StateAnswerVisible,
+ StateNoCards,
+ StatesNum
+ };
+
+public:
+ IStudyWindow(IStudyModel* aModel, QString aStudyName, QWidget* aParent);
+ virtual ~IStudyWindow();
+
+ AppModel::StudyType getStudyType() const { return studyType; }
+ IStudyModel* studyModel() const { return m_model; }
+ const DictTableView* cardEditView() const;
+
+protected:
+ void createUI();
+ virtual QVBoxLayout* createLowerPanel() = 0;
+ virtual void setStateForNextCard();
+ virtual void processState() = 0;
+ virtual void ReadSettings() = 0;
+ virtual QWidget* getAnswerEdit() { return NULL; }
+ virtual QWidget* getUserAnswerLabel() { return NULL; }
+ QString ShortcutToStr( QAbstractButton* aButton );
+ bool bigScreen() { return QApplication::desktop()->screenGeometry().height()> 250; }
+
+private:
+ QVBoxLayout* createMessageLayout();
+ QLabel* createMessageLabel();
+ QWidget* createWrapper(QBoxLayout* layout);
+ QPushButton* createClosePackButton();
+ QHBoxLayout* createUpperPanel();
+ QToolButton* createEditCardButton();
+ QToolButton* createDeleteCardButton();
+ QVBoxLayout* createCardView();
+ QBoxLayout* createAnswerButtonLayout();
+ QPushButton* createAnswerButton();
+ QBoxLayout* createAnswerLayout();
+
+protected slots:
+ void OnDictionaryRemoved();
+ void showNextCard(); ///< Entry point for showing a new card
+ void showAnswer(); ///< Entry point for showing the answer
+ void updateCurCard(); ///< Update the card after pack re-generation
+ void invalidateCurCard() { curCard = NULL; }
+ void openCardEditDialog();
+ void deleteCard();
+ void updateToolBarVisibility(int index);
+
+protected:
+ static const int AnsButtonPage;
+ static const int AnsLabelPage;
+ static const int CardPage;
+ static const int MessagePage;
+
+ AppModel::StudyType studyType;
+ IStudyModel* m_model;
+ const Card* curCard;
+ int state;
+
+ QString m_studyName;
+ QToolButton* editCardBtn;
+ QToolButton* deleteCardBtn;
+
+ QStackedLayout* centralStackedLt;
+ QLabel* messageLabel;
+
+ QStackedLayout* answerStackedLt;
+ QPushButton* answerBtn;
+ CardSideView* questionLabel;
+ CardSideView* answerLabel;
+
+private:
+ static const int MinWidth = 500;
+ static const int MaxWidth = 800;
+ static const int ToolBarButtonSize = 24;
+ static const int ToolBarIconSize = 16;
+ static const int BigButtonWidth = 160;
+ static const int BigButtonHeight = 50;
+
+ QWidget* m_parentWidget;
+ CardEditDialog* m_cardEditDialog;
+};
+
+#endif
diff --git a/src/study/NumberFrame.cpp b/src/study/NumberFrame.cpp
new file mode 100644
index 0000000..1d2a2e1
--- /dev/null
+++ b/src/study/NumberFrame.cpp
@@ -0,0 +1,40 @@
+#include "NumberFrame.h"
+
+NumberFrame::NumberFrame(QWidget* parent):
+ QLabel(parent)
+{
+ init();
+}
+
+void NumberFrame::init()
+{
+ setMinimumSize(MinWidth, MinHeight);
+ setAlignment(Qt::AlignCenter);
+ setFrameShape(QFrame::NoFrame);
+}
+
+void NumberFrame::setColor(const QColor& color)
+{
+ setPalette(QPalette(color));
+}
+
+void NumberFrame::setValue(int value)
+{
+ setMinimumWidth(value >= 100? MinWidth100: MinWidth);
+ setVisible(value > 0);
+ setText(QString::number(value));
+}
+
+void NumberFrame::paintEvent(QPaintEvent* event)
+{
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+
+ QPen pen("#7c7c7c");
+ pen.setWidthF(0.5);
+ painter.setPen(pen);
+ painter.setBrush(QBrush(palette().color(QPalette::Button)));
+ painter.drawRoundedRect(rect().adjusted(1, 1, -1, -1), Radius, Radius);
+
+ QLabel::paintEvent(event);
+}
diff --git a/src/study/NumberFrame.h b/src/study/NumberFrame.h
new file mode 100644
index 0000000..44161ca
--- /dev/null
+++ b/src/study/NumberFrame.h
@@ -0,0 +1,29 @@
+#ifndef NUMBER_FRAME_H
+#define NUMBER_FRAME_H
+
+#include <QtWidgets>
+
+class NumberFrame: public QLabel
+{
+public:
+ static const int MinWidth = 40;
+ static const int MinWidth100 = 50;
+
+public:
+ NumberFrame(QWidget* parent = 0);
+ void setColor(const QColor& color);
+ void setValue(int value);
+
+protected:
+ void paintEvent(QPaintEvent* event);
+
+private:
+ static const int Radius = 7;
+ static const int MinHeight = 16;
+
+private:
+ void init();
+
+};
+
+#endif
diff --git a/src/study/SpacedRepetitionModel.cpp b/src/study/SpacedRepetitionModel.cpp
new file mode 100644
index 0000000..e10299a
--- /dev/null
+++ b/src/study/SpacedRepetitionModel.cpp
@@ -0,0 +1,390 @@
+#include "SpacedRepetitionModel.h"
+
+#include <time.h>
+#include <QtDebug>
+#include <QtCore>
+
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+#include "../utils/IRandomGenerator.h"
+#include "../utils/TimeProvider.h"
+
+SpacedRepetitionModel::SpacedRepetitionModel(CardPack* aCardPack, IRandomGenerator* random):
+ IStudyModel(aCardPack),
+ curCard(NULL), settings(StudySettings::inst()), random(random)
+ {
+ srand(time(NULL));
+ curCardNum = 0;
+ connect(aCardPack, SIGNAL(cardsGenerated()), SLOT(updateStudyState()));
+ updateStudyState();
+ }
+
+SpacedRepetitionModel::~SpacedRepetitionModel()
+ {
+ saveStudy();
+ delete random;
+ }
+
+void SpacedRepetitionModel::saveStudy()
+ {
+ if(!cardPack)
+ return;
+ if(!cardPack->dictionary())
+ return;
+ cardPack->dictionary()->saveStudy();
+ }
+
+// Called after all cards are generated
+void SpacedRepetitionModel::updateStudyState()
+ {
+ curCard = cardPack->getCard( cardPack->findLastReviewedCard() );
+ curCardNum = 0;
+ if( !cardPack->curCardName().isEmpty() )
+ {
+ prevCard = curCard;
+ curCard = cardPack->getCard( cardPack->curCardName() );
+ if( curCard )
+ {
+ answerTime.start();
+ emit curCardUpdated();
+ }
+ else
+ {
+ curCard = prevCard;
+ pickNextCardAndNotify();
+ }
+ }
+ else
+ pickNextCardAndNotify();
+ }
+
+void SpacedRepetitionModel::scheduleCard(int newGrade)
+ {
+ if(!curCard)
+ return;
+ saveStudyRecord(createNewStudyRecord(newGrade));
+ curCardNum++;
+ pickNextCardAndNotify();
+ }
+
+StudyRecord SpacedRepetitionModel::createNewStudyRecord(int newGrade)
+{
+ StudyRecord prevStudy = curCard->getStudyRecord();
+ StudyRecord newStudy;
+
+ newStudy.grade = newGrade;
+ newStudy.level = getNewLevel(prevStudy, newGrade);
+ newStudy.easiness = getNewEasiness(prevStudy, newGrade);
+ newStudy.interval = getNextInterval(prevStudy, newStudy);
+ newStudy.date = TimeProvider::get();
+ newStudy.setRecallTime(curRecallTime / 1000.);
+ newStudy.setAnswerTime(answerTime.elapsed() / 1000.);
+ return newStudy;
+}
+
+int SpacedRepetitionModel::getNewLevel(const StudyRecord& prevStudy, int newGrade)
+{
+ int level = prevStudy.level;
+ switch(newGrade)
+ {
+ case StudyRecord::Unknown:
+ case StudyRecord::Incorrect:
+ level = StudyRecord::ShortLearning;
+ break;
+ case StudyRecord::Difficult:
+ case StudyRecord::Good:
+ if(prevStudy.isOneDayOld())
+ level = StudyRecord::Repeating;
+ else
+ level++;
+ break;
+ case StudyRecord::Easy:
+ level += 2;
+ break;
+ }
+ if(level > StudyRecord::LongLearning)
+ level = StudyRecord::Repeating;
+ return level;
+}
+
+double SpacedRepetitionModel::getNewEasiness(const StudyRecord& prevStudy,
+ int newGrade)
+{
+ switch(prevStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ case StudyRecord::LongLearning:
+ if(prevStudy.isOneDayOld())
+ return getChangeableEasiness(prevStudy, newGrade);
+ else
+ return prevStudy.easiness;
+ case StudyRecord::Repeating:
+ return getChangeableEasiness(prevStudy, newGrade);
+ default:
+ return prevStudy.easiness;
+ }
+}
+
+double SpacedRepetitionModel::getChangeableEasiness(const StudyRecord& prevStudy,
+ int newGrade) const
+{
+ double eas = prevStudy.easiness;
+ switch(newGrade)
+ {
+ case StudyRecord::Difficult:
+ eas += settings->difficultDelta;
+ break;
+ case StudyRecord::Easy:
+ eas += settings->easyDelta;
+ break;
+ default:
+ return prevStudy.easiness;
+ }
+ return limitEasiness(eas);
+}
+
+double SpacedRepetitionModel::limitEasiness(double eas) const
+{
+ if(eas < settings->minEasiness)
+ eas = settings->minEasiness;
+ else if(eas > settings->maxEasiness)
+ eas = settings->maxEasiness;
+ return eas;
+}
+
+double SpacedRepetitionModel::getNextInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ switch(newStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ if(newStudy.grade == StudyRecord::Incorrect)
+ return settings->incorrectInterval;
+ else
+ return settings->unknownInterval;
+ case StudyRecord::LongLearning:
+ return settings->learningInterval;
+ case StudyRecord::Repeating:
+ return getNextRepeatingInterval(prevStudy, newStudy);
+ default:
+ return 0;
+ }
+}
+
+double SpacedRepetitionModel::getNextRepeatingInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ switch(prevStudy.level)
+ {
+ case StudyRecord::ShortLearning:
+ return getNextRepeatingIntervalForShortLearning(prevStudy, newStudy);
+ case StudyRecord::LongLearning:
+ return getNextRepeatingIntervalForLongLearning(prevStudy, newStudy);
+ case StudyRecord::Repeating:
+ return getIncreasedInterval(prevStudy.interval, newStudy.easiness);
+ default:
+ return settings->nextDayInterval;
+ }
+}
+
+double SpacedRepetitionModel::getIncreasedInterval(double prevInterval,
+ double newEasiness)
+{
+ double interval = prevInterval * newEasiness;
+ interval += interval *
+ settings->schedRandomness * random->getInRange_11();
+ return interval;
+}
+
+double SpacedRepetitionModel::getNextRepeatingIntervalForShortLearning(
+ const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ if(prevStudy.isOneDayOld())
+ return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness);
+ else
+ return settings->nextDayInterval;
+}
+
+double SpacedRepetitionModel::getNextRepeatingIntervalForLongLearning(
+ const StudyRecord& prevStudy,
+ const StudyRecord& newStudy)
+{
+ if(prevStudy.isOneDayOld())
+ return getIncreasedInterval(settings->nextDayInterval, newStudy.easiness);
+ else if(newStudy.grade == StudyRecord::Easy)
+ return settings->twoDaysInterval;
+ else
+ return settings->nextDayInterval;
+}
+
+void SpacedRepetitionModel::saveStudyRecord(const StudyRecord& newStudy)
+{
+ cardPack->addStudyRecord(curCard->getQuestion(), newStudy);
+}
+
+QList<int> SpacedRepetitionModel::getAvailableGrades() const
+{
+ if(!curCard)
+ return {};
+ StudyRecord study = curCard->getStudyRecord();
+ switch(study.level)
+ {
+ case StudyRecord::New:
+ return {4, 5};
+ case StudyRecord::ShortLearning:
+ case StudyRecord::LongLearning:
+ if(study.isOneDayOld())
+ return {1, 2, 3, 4, 5};
+ else
+ return {1, 2, 4, 5};
+ case StudyRecord::Repeating:
+ return {1, 2, 3, 4, 5};
+ default:
+ return {};
+ }
+}
+
+bool SpacedRepetitionModel::isNew() const
+{
+ return curCard->getStudyRecord().level == StudyRecord::New;
+}
+
+void SpacedRepetitionModel::pickNextCardAndNotify()
+ {
+ answerTime.start();
+ pickNextCard();
+ if(curCard)
+ cardPack->setCurCard(curCard->getQuestion());
+ else
+ cardPack->setCurCard("");
+ // Notify the study window to show the selected card.
+ emit nextCardSelected();
+ }
+
+void SpacedRepetitionModel::pickNextCard()
+{
+ prevCard = curCard;
+ curCard = NULL;
+ pickActiveCard() ||
+ pickNewCard() ||
+ pickLearningCard();
+}
+
+bool SpacedRepetitionModel::mustRandomPickScheduledCard() const
+{
+ return random->getInRange_01() > settings->newCardsShare;
+}
+
+bool SpacedRepetitionModel::reachedNewCardsDayLimit() const
+{
+ return cardPack->getTodayNewCardsNum() >= settings->newCardsDayLimit;
+}
+
+bool SpacedRepetitionModel::pickActiveCard()
+{
+ QStringList activeCards = cardPack->getActiveCards();
+ if(activeCards.isEmpty())
+ return false;
+ if(pickPriorityActiveCard())
+ return true;
+ if(!mustPickScheduledCard())
+ return false;
+ curCard = cardPack->getCard(getRandomStr(activeCards));
+ return true;
+}
+
+bool SpacedRepetitionModel::pickPriorityActiveCard()
+{
+ QStringList priorityCards = cardPack->getPriorityActiveCards();
+ if(priorityCards.isEmpty())
+ return false;
+ QStringList smallestIntervals = cardPack->getSmallestIntervalCards(priorityCards);
+ curCard = cardPack->getCard(getRandomStr(smallestIntervals));
+ return true;
+}
+
+bool SpacedRepetitionModel::mustPickScheduledCard()
+{
+ bool noNewCards = cardPack->getNewCards().isEmpty();
+ if(noNewCards || reachedNewCardsDayLimit() ||
+ tooManyScheduledCards())
+ return true;
+ else
+ return mustRandomPickScheduledCard();
+}
+
+bool SpacedRepetitionModel::tooManyScheduledCards() const
+{
+ return cardPack->countScheduledForTodayCards() >= settings->limitForAddingNewCards;
+}
+
+bool SpacedRepetitionModel::pickNewCard()
+ {
+ if(reachedNewCardsDayLimit())
+ return false;
+ QStringList newCards = cardPack->getNewCards();
+ if(newCards.isEmpty())
+ return false;
+ QString cardName;
+ if(settings->showRandomly)
+ cardName = getRandomStr(newCards);
+ else
+ cardName = newCards.first();
+ curCard = cardPack->getCard(cardName);
+ return true;
+ }
+
+bool SpacedRepetitionModel::pickLearningCard()
+{
+ QStringList learningCards = cardPack->getLearningCards();
+ if(learningCards.isEmpty())
+ return false;
+ QStringList smallestIntervals = cardPack->getSmallestIntervalCards(learningCards);
+ curCard = cardPack->getCard(getRandomStr(smallestIntervals));
+ return true;
+}
+
+QString SpacedRepetitionModel::getRandomStr(const QStringList& list) const
+{
+ return list.at(random->getRand(list.size()));
+}
+
+/// New cards inside the reviewed ones still today
+int SpacedRepetitionModel::estimatedNewReviewedCardsToday() const
+ {
+ if(tooManyScheduledCards())
+ return 0;
+ int scheduledToday = cardPack->countScheduledForTodayCards();
+ int newRev = 0;
+ if( scheduledToday > 0 )
+ {
+ float newShare = settings->newCardsShare;
+ newRev = qRound( scheduledToday * newShare );
+ }
+ else
+ return 0;
+
+ // Check for remained new cards in pack
+ int newCardsNum = cardPack->getNewCards().size();
+ if( newRev > newCardsNum )
+ newRev = newCardsNum;
+
+ // Check for new cards day limit
+ int newCardsDayLimit = settings->newCardsDayLimit;
+ int todayReviewedNewCards = cardPack->getTodayNewCardsNum();
+ int remainedNewCardsLimit = newCardsDayLimit - todayReviewedNewCards;
+ if( newRev > remainedNewCardsLimit )
+ newRev = remainedNewCardsLimit;
+
+ return newRev;
+ }
+
+/** Calculates number of candidate cards to be shown in the current session.
+ */
+int SpacedRepetitionModel::countTodayRemainingCards() const
+ {
+ int timeTriggered = cardPack->getActiveCards().size();
+ return timeTriggered + estimatedNewReviewedCardsToday();
+ }
diff --git a/src/study/SpacedRepetitionModel.h b/src/study/SpacedRepetitionModel.h
new file mode 100644
index 0000000..d39a1b8
--- /dev/null
+++ b/src/study/SpacedRepetitionModel.h
@@ -0,0 +1,79 @@
+#ifndef SPACEDREPETITIONMODEL_H
+#define SPACEDREPETITIONMODEL_H
+
+#include "IStudyModel.h"
+#include "StudyRecord.h"
+#include "StudySettings.h"
+
+#include <QMultiMap>
+#include <QTime>
+
+class CardPack;
+class IRandomGenerator;
+
+class SpacedRepetitionModel: public IStudyModel
+{
+ Q_OBJECT
+
+public:
+ static const int NewCardsDayLimit = 10;
+
+public:
+ SpacedRepetitionModel(CardPack* aCardPack, IRandomGenerator* random);
+ ~SpacedRepetitionModel();
+
+public:
+ Card* getCurCard() const { return curCard; }
+ QList<int> getAvailableGrades() const;
+ bool isNew() const;
+ void setRecallTime( int aTime ) { curRecallTime = aTime; }
+ int estimatedNewReviewedCardsToday() const;
+ int countTodayRemainingCards() const;
+
+public slots:
+ void scheduleCard(int newGrade);
+
+protected:
+ void pickNextCardAndNotify();
+
+private:
+ StudyRecord createNewStudyRecord(int newGrade);
+ int getNewLevel(const StudyRecord& prevStudy, int newGrade);
+ double getNewEasiness(const StudyRecord& prevStudy, int newGrade);
+ double getChangeableEasiness(const StudyRecord& prevStudy, int newGrade) const;
+ double limitEasiness(double eas) const;
+ double getNextInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy);
+ double getNextRepeatingInterval(const StudyRecord& prevStudy,
+ const StudyRecord& newStudy);
+ double getIncreasedInterval(double prevInterval, double newEasiness);
+ double getNextRepeatingIntervalForShortLearning(
+ const StudyRecord& prevStudy, const StudyRecord& newStudy);
+ double getNextRepeatingIntervalForLongLearning(
+ const StudyRecord& prevStudy, const StudyRecord& newStudy);
+ void saveStudyRecord(const StudyRecord& newStudy);
+ void pickNextCard();
+ bool pickNewCard();
+ QString getRandomStr(const QStringList& list) const;
+ bool reachedNewCardsDayLimit() const;
+ bool mustPickScheduledCard();
+ bool tooManyScheduledCards() const;
+ bool mustRandomPickScheduledCard() const;
+ bool pickActiveCard();
+ bool pickPriorityActiveCard();
+ bool pickLearningCard();
+ void saveStudy();
+
+private slots:
+ void updateStudyState();
+
+private:
+ Card* curCard; ///< The card selected for repetition
+ Card* prevCard; ///< Previous reviewed card. Found in the study history.
+ int curRecallTime; ///< Recall time of the current card, ms
+ QTime answerTime; ///< Full answer time of the current card: recall + evaluation, ms
+ StudySettings* settings;
+ IRandomGenerator* random;
+};
+
+#endif
diff --git a/src/study/SpacedRepetitionWindow.cpp b/src/study/SpacedRepetitionWindow.cpp
new file mode 100644
index 0000000..977b1ec
--- /dev/null
+++ b/src/study/SpacedRepetitionWindow.cpp
@@ -0,0 +1,380 @@
+#include "SpacedRepetitionWindow.h"
+#include "CardsStatusBar.h"
+#include "NumberFrame.h"
+#include "WarningPanel.h"
+#include "../dictionary/Dictionary.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+
+SpacedRepetitionWindow::SpacedRepetitionWindow( SpacedRepetitionModel* aModel, QWidget* aParent ):
+ IStudyWindow( aModel, tr("Spaced repetition"), aParent ),
+ exactAnswerLabel(NULL), exactAnswerEdit(NULL)
+{
+ usesExactAnswer = m_model->getCardPack()->getUsesExactAnswer();
+ studyType = AppModel::SpacedRepetition;
+ if(usesExactAnswer)
+ createExactAnswerWidget();
+ createUI(); // Can't call from parent constructor
+ setWindowIcon( QIcon(":/images/spaced-rep.png") );
+ dayCardsLimitShown = false;
+ showNextCard();
+}
+
+SpacedRepetitionWindow::~SpacedRepetitionWindow()
+{
+ WriteSettings();
+}
+
+void SpacedRepetitionWindow::createExactAnswerWidget()
+{
+ exactAnswerEdit = new QLineEdit;
+
+ exactAnswerLabel = new CardSideView;
+ exactAnswerLabel->setMaximumHeight(40);
+ exactAnswerLabel->setShowMode(CardSideView::AnsMode);
+ exactAnswerLabel->setPack(m_model->getCardPack());
+}
+
+QVBoxLayout* SpacedRepetitionWindow::createLowerPanel()
+{
+ warningPanel = new WarningPanel;
+
+ controlLt = new QVBoxLayout;
+ controlLt->addWidget(warningPanel);
+ controlLt->addLayout(createProgressLayout());
+ controlLt->addLayout(new QVBoxLayout); // Grades layout
+
+ createGradeButtons();
+
+ return controlLt;
+}
+
+QBoxLayout* SpacedRepetitionWindow::createProgressLayout()
+{
+ QVBoxLayout* lt = new QVBoxLayout;
+ lt->setSpacing(3);
+ lt->addLayout(createStatsLayout());
+ lt->addLayout(createProgressBarLayout());
+ return lt;
+}
+
+QBoxLayout* SpacedRepetitionWindow::createStatsLayout()
+{
+ todayNewLabel = new NumberFrame;
+ todayNewLabel->setToolTip(tr("Today learned new cards"));
+ todayNewLabel->setColor("#c0d6ff");
+
+ learningCardsLabel = new NumberFrame;
+ learningCardsLabel->setToolTip(tr("Scheduled learning reviews:\n"
+ "new cards must be repeated today to learn"));
+ learningCardsLabel->setColor(CardsStatusBar::Colors[1]);
+
+ timeToNextLearningLabel = new QLabel;
+ timeToNextLearningLabel->setToolTip(tr("Time left to the next learning review"));
+
+ scheduledCardsLabel = new NumberFrame;
+ scheduledCardsLabel->setToolTip(tr("Scheduled cards for today"));
+ scheduledCardsLabel->setColor(CardsStatusBar::Colors[2]);
+
+ scheduledNewLabel = new NumberFrame;
+ scheduledNewLabel->setToolTip(tr("New scheduled cards for today:\n"
+ "new cards that will be shown between the scheduled ones"));
+ scheduledNewLabel->setColor("#bd8d71");
+
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->setSpacing(5);
+ lt->addWidget(todayNewLabel);
+ lt->addStretch();
+ lt->addWidget(learningCardsLabel);
+ lt->addWidget(timeToNextLearningLabel);
+ lt->addWidget(scheduledCardsLabel);
+ lt->addWidget(scheduledNewLabel);
+ return lt;
+}
+
+QBoxLayout* SpacedRepetitionWindow::createProgressBarLayout()
+{
+ progressLabel = new QLabel;
+ progressLabel->setToolTip(getProgressBarTooltip());
+
+ coloredProgressBar = new CardsStatusBar;
+ coloredProgressBar->setToolTip(progressLabel->toolTip());
+
+ QHBoxLayout* lt = new QHBoxLayout;
+ lt->setSpacing(5);
+ lt->addWidget(progressLabel);
+ lt->addWidget(coloredProgressBar);
+ return lt;
+}
+
+QString SpacedRepetitionWindow::getProgressBarTooltip()
+{
+ QString boxSpace;
+ for(int i = 0; i < 6; i++)
+ boxSpace += "&nbsp;";
+ QString colorBoxPattern = "<p><span style=\"background-color: %1;\">" +
+ boxSpace + "</span> &nbsp;";
+ QString pEnd = "</p>";
+
+ QStringList legendLines = {
+ tr("Reviewed cards"),
+ tr("Learning reviews"),
+ tr("Scheduled cards"),
+ tr("New cards for today")};
+ QString legend;
+ for(int i = 0; i < 4; i++)
+ legend += colorBoxPattern.arg(CardsStatusBar::Colors[i]) + legendLines[i] + pEnd;
+ return tr("Progress of reviews scheduled for today:") + legend;
+}
+
+void SpacedRepetitionWindow::createGradeButtons()
+{
+ cardGradedSM = new QSignalMapper(this);
+ connect( cardGradedSM, SIGNAL(mapped(int)),
+ qobject_cast<SpacedRepetitionModel*>(m_model), SLOT(scheduleCard(int)) );
+
+ gradeBtns[0] = createGradeButton(0, "question.png", tr("Unknown"),
+ tr("Completely forgotten card, couldn't recall the answer."));
+ gradeBtns[1] = createGradeButton(1, "red-stop.png", tr("Incorrect"),
+ tr("The answer is incorrect."));
+ gradeBtns[2] = createGradeButton(2, "blue-triangle-down.png", tr("Difficult"),
+ tr("It's difficult to recall the answer. The last interval was too long."));
+ gradeBtns[3] = createGradeButton(3, "green-tick.png", tr("Good"),
+ tr("The answer is recalled in couple of seconds. The last interval was good enough."));
+ gradeBtns[4] = createGradeButton(4, "green-triangle-up.png", tr("Easy"),
+ tr("The card is too easy, and recalled without any effort. The last interval was too short."));
+ goodBtn = gradeBtns[StudyRecord::Good - 1];
+}
+
+QPushButton* SpacedRepetitionWindow::createGradeButton(int i, const QString& iconName,
+ const QString& label, const QString& toolTip)
+{
+ QPushButton* btn = new QPushButton;
+ btn->setIcon(QIcon(":/images/" + iconName));
+ btn->setText(QString("%1 %2").arg(i + 1).arg(label));
+ btn->setToolTip("<p>" + toolTip);
+ btn->setShortcut( QString::number(i + 1) );
+ btn->setMinimumWidth(GradeButtonMinWidth);
+
+#if defined(Q_OS_WIN)
+ QFont font = btn->font();
+ font.setPointSize(12);
+ font.setFamily("Calibri");
+ btn->setFont(font);
+#endif
+
+ cardGradedSM->setMapping(btn, i + 1);
+ connect(btn, SIGNAL(clicked()), cardGradedSM, SLOT(map()));
+ return btn;
+}
+
+void SpacedRepetitionWindow::processState()
+{
+ setEnabledGradeButtons();
+ switch(state)
+ {
+ case StateAnswerHidden:
+ displayQuestion();
+ break;
+ case StateAnswerVisible:
+ displayAnswer();
+ break;
+ case StateNoCards:
+ updateCardsProgress();
+ displayNoRemainedCards();
+ break;
+ }
+}
+
+void SpacedRepetitionWindow::setEnabledGradeButtons()
+{
+ for(int i = 0; i < StudyRecord::GradesNum; i++)
+ gradeBtns[i]->setEnabled(state == StateAnswerVisible);
+}
+
+void SpacedRepetitionWindow::displayQuestion()
+{
+ questionLabel->setQuestion( m_model->getCurCard()->getQuestion() );
+ answerStackedLt->setCurrentIndex( AnsButtonPage );
+ updateCardsProgress();
+ layoutGradeButtons();
+ focusAnswerWidget();
+ m_answerTime.start();
+ processIsNewCard();
+}
+
+void SpacedRepetitionWindow::layoutGradeButtons()
+{
+ QHBoxLayout* higherLt = new QHBoxLayout;
+ higherLt->addStretch();
+ putGradeButtonsIntoLayouts(higherLt);
+ higherLt->addStretch();
+ replaceGradesLayout(higherLt);
+ setGoodButtonText();
+}
+
+void SpacedRepetitionWindow::putGradeButtonsIntoLayouts(QBoxLayout* higherLt)
+{
+ QList<int> visibleGrades = static_cast<SpacedRepetitionModel*>(m_model)
+ ->getAvailableGrades();
+ for(int i = 0; i < StudyRecord::GradesNum; i++)
+ {
+ if(!visibleGrades.contains(i + 1))
+ {
+ gradeBtns[i]->setParent(NULL);
+ continue;
+ }
+ higherLt->addWidget(gradeBtns[i]);
+ }
+}
+
+void SpacedRepetitionWindow::replaceGradesLayout(QBoxLayout* higherLt)
+{
+ const int GradesLtIndex = 2;
+ delete controlLt->takeAt(GradesLtIndex);
+ controlLt->insertLayout(GradesLtIndex, higherLt);
+}
+
+void SpacedRepetitionWindow::setGoodButtonText()
+{
+ QList<int> visibleGrades = static_cast<SpacedRepetitionModel*>(m_model)
+ ->getAvailableGrades();
+ QString goodLabel = visibleGrades.size() == StudyRecord::GradesNum?
+ tr("Good") : tr("OK");
+ goodBtn->setText(QString("%1 %2").arg(StudyRecord::Good).arg(goodLabel));
+ goodBtn->setShortcut(QString::number(StudyRecord::Good));
+}
+
+void SpacedRepetitionWindow::focusAnswerWidget()
+{
+ if(usesExactAnswer)
+ {
+ exactAnswerEdit->clear();
+ exactAnswerEdit->setFocus();
+ }
+ else
+ answerBtn->setFocus();
+}
+
+void SpacedRepetitionWindow::processIsNewCard()
+{
+ questionLabel->showNewIcon(static_cast<SpacedRepetitionModel*>(m_model)->isNew());
+}
+
+void SpacedRepetitionWindow::displayAnswer()
+{
+ static_cast<SpacedRepetitionModel*>(m_model)->setRecallTime(m_answerTime.elapsed());
+ Card* card = m_model->getCurCard();
+ QStringList correctAnswers = card->getAnswers();
+ answerStackedLt->setCurrentIndex(AnsLabelPage);
+ if(usesExactAnswer)
+ showExactAnswer(correctAnswers);
+ answerLabel->setQstAnsr(card->getQuestion(), correctAnswers);
+ goodBtn->setFocus();
+}
+
+void SpacedRepetitionWindow::showExactAnswer(const QStringList& correctAnswers)
+{
+ if(static_cast<SpacedRepetitionModel*>(m_model)->isNew())
+ {
+ exactAnswerLabel->hide();
+ return;
+ }
+ else
+ exactAnswerLabel->show();
+ bool isCorrect = correctAnswers.first() == exactAnswerEdit->text().trimmed();
+ QString beginning("<span style=\"");
+ beginning += QString("color:%1").arg(isCorrect ? "green" : "red");
+ beginning += "\">";
+ QString answer = beginning + exactAnswerEdit->text() + "</span>";
+ exactAnswerLabel->setQstAnsr("", {answer});
+}
+
+void SpacedRepetitionWindow::updateCardsProgress()
+ {
+ CardPack* pack = m_model->getCardPack();
+ int todayReviewed = pack->getTodayReviewedCardsNum();
+ int todayNewReviewed = pack->getTodayNewCardsNum();
+ int activeRepeating = pack->getActiveRepeatingCardsNum();
+ int learningReviews = pack->getLearningReviewsNum();
+ int timeToNextLearning = pack->getTimeToNextLearning();
+ int scheduledNew = static_cast<SpacedRepetitionModel*>(m_model)->
+ estimatedNewReviewedCardsToday();
+ int allTodaysCards = todayReviewed + learningReviews +
+ activeRepeating + scheduledNew;
+
+ todayNewLabel->setVisible(todayNewReviewed > 0);
+ todayNewLabel->setValue(todayNewReviewed);
+ learningCardsLabel->setValue(learningReviews);
+
+ int minsToNextLearning = timeToNextLearning / 60;
+ timeToNextLearningLabel->setVisible(minsToNextLearning > 0);
+ timeToNextLearningLabel->setText(tr("(%1 min)").
+ arg(minsToNextLearning));
+
+ scheduledCardsLabel->setValue(activeRepeating);
+ scheduledNewLabel->setValue(scheduledNew);
+
+ progressLabel->setText(QString("%1/%2").
+ arg(todayReviewed).arg(allTodaysCards));
+ coloredProgressBar->setValues({todayReviewed, learningReviews,
+ activeRepeating, scheduledNew});
+
+ showLimitWarnings();
+}
+
+void SpacedRepetitionWindow::showLimitWarnings()
+{
+ if(warningPanel->isVisible())
+ return;
+ CardPack* pack = m_model->getCardPack();
+ if(dayLimitReached(pack->dictionary()->countTodaysAllCards()))
+ {
+ warningPanel->setText(tr("Day cards limit is reached: %1 cards.\n"
+ "It is recommended to stop studying this dictionary.")
+ .arg(StudySettings::inst()->cardsDayLimit));
+ warningPanel->show();
+ dayCardsLimitShown = true;
+ }
+}
+
+bool SpacedRepetitionWindow::dayLimitReached(int todayReviewed)
+{
+ return todayReviewed >= StudySettings::inst()->cardsDayLimit &&
+ !dayCardsLimitShown;
+}
+
+void SpacedRepetitionWindow::displayNoRemainedCards()
+{
+ centralStackedLt->setCurrentIndex(MessagePage);
+ messageLabel->setText(
+ QString("<div align=\"center\" style=\"font-size: 22pt; font-weight: bold\">") +
+ tr("All cards are reviewed") +
+ "</div>" + "<p align=\"center\" style=\"font-size: 11pt\">" +
+ tr("You can go to the next pack or dictionary, or open the Word drill.") +
+ "</p>");
+}
+
+void SpacedRepetitionWindow::ReadSettings()
+{
+ QSettings settings;
+ move( settings.value("spacedrep-pos", QPoint(PosX, PosY)).toPoint() );
+ resize( settings.value("spacedrep-size", QSize(Width, Height)).toSize() );
+}
+
+void SpacedRepetitionWindow::WriteSettings()
+{
+ QSettings settings;
+ settings.setValue("spacedrep-pos", pos());
+ settings.setValue("spacedrep-size", size());
+}
+
+QWidget* SpacedRepetitionWindow::getAnswerEdit()
+{
+ return exactAnswerEdit;
+}
+
+QWidget* SpacedRepetitionWindow::getUserAnswerLabel()
+{
+ return exactAnswerLabel;
+}
diff --git a/src/study/SpacedRepetitionWindow.h b/src/study/SpacedRepetitionWindow.h
new file mode 100644
index 0000000..942b7f2
--- /dev/null
+++ b/src/study/SpacedRepetitionWindow.h
@@ -0,0 +1,89 @@
+#ifndef SPACED_REPETITION_WINDOW_H
+#define SPACED_REPETITION_WINDOW_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "IStudyWindow.h"
+#include "StudyRecord.h"
+#include "StudySettings.h"
+#include "SpacedRepetitionModel.h"
+#include "CardSideView.h"
+
+class CardsStatusBar;
+class NumberFrame;
+class WarningPanel;
+
+class SpacedRepetitionWindow: public IStudyWindow
+{
+ Q_OBJECT
+
+public:
+ SpacedRepetitionWindow( SpacedRepetitionModel* aModel, QWidget* aParent );
+ ~SpacedRepetitionWindow();
+
+protected:
+ QVBoxLayout* createLowerPanel();
+ void processState();
+ void ReadSettings();
+ void WriteSettings();
+ QWidget* getAnswerEdit();
+ QWidget* getUserAnswerLabel();
+
+private:
+ static QString getProgressBarTooltip();
+
+private:
+ QBoxLayout* createProgressLayout();
+ QBoxLayout* createStatsLayout();
+ QBoxLayout* createProgressBarLayout();
+ void createGradeButtons();
+ QPushButton* createGradeButton(int i, const QString& iconName,
+ const QString& label, const QString& toolTip);
+ void setEnabledGradeButtons();
+ void layoutGradeButtons();
+ void putGradeButtonsIntoLayouts(QBoxLayout* higherLt);
+ void replaceGradesLayout(QBoxLayout* higherLt);
+ void setGoodButtonText();
+ void updateCardsProgress();
+ void displayAnswer();
+ void displayNoRemainedCards();
+ void showLimitWarnings();
+ bool dayLimitReached(int todayReviewed);
+ void createExactAnswerWidget();
+ void showExactAnswer(const QStringList& correctAnswers);
+ void focusAnswerWidget();
+ void processIsNewCard();
+
+private slots:
+ void displayQuestion();
+
+private:
+ static const int PosX = 200;
+ static const int PosY = 200;
+ static const int Width = 600;
+ static const int Height = 430;
+ static const int GradeButtonMinWidth = 100;
+
+private:
+ bool dayCardsLimitShown;
+
+ WarningPanel* warningPanel;
+ QLabel* progressLabel;
+ NumberFrame* todayNewLabel;
+ NumberFrame* learningCardsLabel;
+ QLabel* timeToNextLearningLabel;
+ NumberFrame* scheduledCardsLabel;
+ NumberFrame* scheduledNewLabel;
+ QVBoxLayout* controlLt;
+ CardsStatusBar* coloredProgressBar;
+ QPushButton* gradeBtns[StudyRecord::GradesNum];
+ QPushButton* goodBtn;
+ QSignalMapper* cardGradedSM;
+ bool usesExactAnswer;
+ CardSideView* exactAnswerLabel;
+ QLineEdit* exactAnswerEdit;
+ QTime m_answerTime;
+};
+
+#endif
diff --git a/src/study/StudyFileReader.cpp b/src/study/StudyFileReader.cpp
new file mode 100644
index 0000000..ecc4dbf
--- /dev/null
+++ b/src/study/StudyFileReader.cpp
@@ -0,0 +1,238 @@
+#include "StudyFileReader.h"
+#include "../dictionary/Dictionary.h"
+#include "../version.h"
+#include "../dictionary/CardPack.h"
+#include "../dictionary/DicRecord.h"
+
+#include <QMessageBox>
+
+const QString StudyFileReader::MinSupportedStudyVersion = "1.0";
+
+StudyFileReader::StudyFileReader( Dictionary* aDict ):
+ m_dict( aDict ), m_cardPack( NULL )
+ {
+ settings = StudySettings::inst();
+ }
+
+bool StudyFileReader::read( QIODevice* aDevice )
+ {
+ setDevice( aDevice );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isStartElement() )
+ {
+ if( name() == "study" )
+ {
+ readStudy();
+ return !error(); // readStudy() processes all situations
+ }
+ else
+ raiseError( Dictionary::tr("The file is not a study file.") );
+ }
+ }
+ return !error();
+ }
+
+void StudyFileReader::readStudy()
+ {
+ Q_ASSERT( isStartElement() && name() == "study" );
+
+ m_studyVersion = attributes().value( "version" ).toString();
+ if(m_studyVersion >= MinSupportedStudyVersion)
+ readStudyCurrentVersion();
+ else
+ QMessageBox::warning( NULL, Dictionary::tr("Unsupported format"),
+ Dictionary::tr("The study file uses unsupported format %1.\n"
+ "The minimum supported version is %2" )
+ .arg( m_studyVersion )
+ .arg( MinSupportedStudyVersion ) );
+ }
+
+void StudyFileReader::readUnknownElement()
+ {
+ Q_ASSERT( isStartElement() );
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ readUnknownElement();
+ }
+ }
+
+void StudyFileReader::readStudyCurrentVersion()
+ {
+ Q_ASSERT( isStartElement() && name() == "study" );
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "pack" )
+ readPack();
+ else
+ readUnknownElement();
+ }
+ }
+ if(m_studyVersion != STUDY_VERSION)
+ m_dict->setStudyModified();
+ }
+
+void StudyFileReader::readPack()
+ {
+ Q_ASSERT( isStartElement() && name() == "pack" );
+
+ QString id = attributes().value( "id" ).toString();
+ m_cardPack = m_dict->cardPack( id );
+ if( !m_cardPack )
+ {
+ skipCurrentElement();
+ return;
+ }
+ m_cardPack->setReadingStudyFile(true);
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "cur-card" )
+ {
+ Q_ASSERT( m_cardPack->curCardName().isEmpty() );
+ QString curCard = attributes().value( "id" ).toString();
+ if( !curCard.isEmpty() )
+ m_cardPack->setCurCard( curCard );
+ readNext(); // read end of 'cur-card' element
+ Q_ASSERT( isEndElement() );
+ }
+ else if( name() == "c" )
+ readC();
+ else
+ readUnknownElement();
+ }
+ }
+
+ m_cardPack->setReadingStudyFile(false);
+ }
+
+void StudyFileReader::readC()
+ {
+ Q_ASSERT( isStartElement() && name() == "c" && m_cardPack );
+
+ m_cardId = attributes().value( "id" ).toString();
+
+ while( !atEnd() )
+ {
+ readNext();
+ if( isEndElement() )
+ break;
+ if( isStartElement() )
+ {
+ if( name() == "r" )
+ readR();
+ else
+ readUnknownElement();
+ }
+ }
+ }
+
+void StudyFileReader::readR()
+ {
+ Q_ASSERT( isStartElement() && name() == "r" );
+
+ StudyRecord study;
+ bool ok; // for number conversions
+
+ QString date = attributes().value( "d" ).toString();
+ study.date = QDateTime::fromString( date, Qt::ISODate );
+
+ QString grade = attributes().value( "g" ).toString();
+ study.grade = grade.toInt( &ok );
+ if(m_studyVersion < "1.4" && study.grade < 3)
+ study.grade++;
+
+ QString easiness = attributes().value( "e" ).toString();
+ study.easiness = easiness.toFloat( &ok );
+
+ QString time;
+ time = attributes().value( "t" ).toString(); // obsolete parameter
+ if(!time.isEmpty())
+ {
+ study.recallTime = time.toFloat( &ok );
+ Q_ASSERT( ok );
+ }
+
+ time = attributes().value( "rt" ).toString();
+ if(!time.isEmpty())
+ {
+ study.recallTime = time.toFloat( &ok );
+ if( study.recallTime > StudyRecord::MaxAnswerTime )
+ study.recallTime = StudyRecord::MaxAnswerTime;
+ Q_ASSERT( ok );
+ }
+
+ time = attributes().value( "at" ).toString();
+ if(!time.isEmpty())
+ {
+ study.answerTime = time.toFloat( &ok );
+ if( study.answerTime > StudyRecord::MaxAnswerTime )
+ study.answerTime = StudyRecord::MaxAnswerTime;
+ Q_ASSERT( ok );
+ }
+
+ QString interval = attributes().value( "i" ).toString();
+ QString level = attributes().value( "l" ).toString();
+ if(!interval.isEmpty() && interval != "0")
+ {
+ study.interval = interval.toFloat();
+ if(!level.isEmpty())
+ study.level = level.toInt();
+ else
+ study.level = StudyRecord::Repeating;
+ if(m_studyVersion == "1.4")
+ fixupIncorrectGradeIn1_4(study);
+ }
+ else // delayed card
+ {
+ QString cardsStr = attributes().value( "c" ).toString();
+ if( !cardsStr.isEmpty() )
+ {
+ int cardInterval = cardsStr.toInt();
+ if(cardInterval <= 7)
+ study.interval = settings->unknownInterval;
+ else
+ study.interval = settings->incorrectInterval;
+ }
+ else
+ study.interval = 0;
+ study.level = StudyRecord::ShortLearning;
+ }
+
+ m_cardPack->addStudyRecord( m_cardId, study );
+
+ readElementText(); // read until element end
+ }
+
+void StudyFileReader::fixupIncorrectGradeIn1_4(StudyRecord& study)
+{
+ if(!(study.level == StudyRecord::ShortLearning &&
+ study.grade <= 3))
+ return;
+ if(equalDouble(study.interval, settings->unknownInterval))
+ study.grade = StudyRecord::Unknown;
+ else if(equalDouble(study.interval, settings->incorrectInterval))
+ study.grade = StudyRecord::Incorrect;
+}
+
+bool StudyFileReader::equalDouble(double a, double b)
+{
+ static const double Dif = 0.0001;
+ return fabs(a - b) < Dif;
+}
diff --git a/src/study/StudyFileReader.h b/src/study/StudyFileReader.h
new file mode 100644
index 0000000..2466fe3
--- /dev/null
+++ b/src/study/StudyFileReader.h
@@ -0,0 +1,44 @@
+#ifndef STUDYFILEREADER_H
+#define STUDYFILEREADER_H
+
+#include <QXmlStreamReader>
+#include <QList>
+#include <QString>
+
+#include "StudySettings.h"
+#include "StudyRecord.h"
+
+class Dictionary;
+class CardPack;
+class DicRecord;
+
+class StudyFileReader : public QXmlStreamReader
+{
+public:
+ StudyFileReader( Dictionary* aDict );
+ bool read( QIODevice* aDevice );
+
+private:
+ static const QString MinSupportedStudyVersion;
+
+private:
+ void readStudy();
+ void readUnknownElement();
+ void readStudyCurrentVersion();
+ void readPack();
+ void readC();
+ void readR();
+ void fixupIncorrectGradeIn1_4(StudyRecord& study);
+
+private:
+ static bool equalDouble(double a, double b);
+
+private:
+ Dictionary* m_dict;
+ QString m_studyVersion;
+ CardPack* m_cardPack;
+ QString m_cardId;
+ StudySettings* settings;
+};
+
+#endif // STUDYFILEREADER_H
diff --git a/src/study/StudyFileWriter.cpp b/src/study/StudyFileWriter.cpp
new file mode 100644
index 0000000..ab68d6f
--- /dev/null
+++ b/src/study/StudyFileWriter.cpp
@@ -0,0 +1,68 @@
+#include "StudyFileWriter.h"
+#include "StudySettings.h"
+#include "../dictionary/Dictionary.h"
+#include "../version.h"
+#include "../dictionary/CardPack.h"
+
+StudyFileWriter::StudyFileWriter( const Dictionary* aDict ):
+ m_dict( aDict )
+{
+ setAutoFormatting( true );
+}
+
+bool StudyFileWriter::write( QIODevice* aDevice )
+{
+ setDevice( aDevice );
+ writeStartDocument();
+ writeDTD( "<!DOCTYPE freshmemory-study>" );
+ writeStartElement("study");
+ writeAttribute( "version", STUDY_VERSION );
+
+ foreach( CardPack* pack, m_dict->cardPacks() )
+ writePack( pack );
+
+ writeEndDocument();
+ return true;
+}
+
+void StudyFileWriter::writePack( const CardPack* aPack )
+ {
+ QStringList cardIds = aPack->getCardQuestions();
+ if( cardIds.isEmpty() )
+ return; // Don't write empty pack
+ writeStartElement( "pack" );
+ writeAttribute( "id", aPack->id() );
+ if( !aPack->curCardName().isEmpty() )
+ {
+ writeEmptyElement( "cur-card" );
+ writeAttribute( "id", aPack->curCardName() );
+ }
+ foreach( QString cardId, cardIds )
+ writeCard( cardId, aPack );
+ writeEndElement();
+ }
+
+void StudyFileWriter::writeCard( const QString& aCardId, const CardPack* aPack )
+ {
+ QList<StudyRecord> studyRecords( aPack->getStudyRecords( aCardId ) );
+ if( studyRecords.isEmpty() ) // Don't write cards without records
+ return;
+ writeStartElement( "c" );
+ writeAttribute( "id", aCardId );
+ // Take study records from the list in reverse order. The first is the most recent.
+ QListIterator<StudyRecord> it( studyRecords );
+ it.toBack();
+ while( it.hasPrevious() )
+ {
+ StudyRecord record = it.previous();
+ writeEmptyElement( "r" );
+ writeAttribute( "d", record.date.toString( Qt::ISODate ) );
+ writeAttribute( "l", QString::number( record.level ) );
+ writeAttribute( "g", QString::number( record.grade ) );
+ writeAttribute( "e", QString::number( record.easiness ) );
+ writeAttribute( "rt", QString::number( record.recallTime, 'g', 4 ) );
+ writeAttribute( "at", QString::number( record.answerTime, 'g', 4 ) );
+ writeAttribute( "i", QString::number( record.interval, 'g', 6 ) );
+ }
+ writeEndElement(); // <c />
+ }
diff --git a/src/study/StudyFileWriter.h b/src/study/StudyFileWriter.h
new file mode 100644
index 0000000..8800846
--- /dev/null
+++ b/src/study/StudyFileWriter.h
@@ -0,0 +1,26 @@
+#ifndef STUDYFILEWRITER_H
+#define STUDYFILEWRITER_H
+
+#include <QXmlStreamWriter>
+#include <QString>
+#include <QList>
+
+#include "StudyRecord.h"
+
+class Dictionary;
+class CardPack;
+
+class StudyFileWriter : public QXmlStreamWriter
+{
+public:
+ StudyFileWriter( const Dictionary* aDict );
+ bool write( QIODevice* aDevice );
+
+private:
+ void writePack( const CardPack* aPack );
+ void writeCard(const QString& aCardId, const CardPack* aPack );
+private:
+ const Dictionary* m_dict;
+};
+
+#endif // STUDYFILEWRITER_H
diff --git a/src/study/StudyRecord.cpp b/src/study/StudyRecord.cpp
new file mode 100644
index 0000000..330f572
--- /dev/null
+++ b/src/study/StudyRecord.cpp
@@ -0,0 +1,129 @@
+#include "StudyRecord.h"
+
+#include "StudySettings.h"
+#include "../utils/TimeProvider.h"
+
+ostream& operator<<(ostream& os, const StudyRecord& study)
+{
+ if(study.date <= QDateTime::currentDateTime())
+ {
+ const char* dateStr = study.date.toString(Qt::ISODate).toStdString().c_str();
+ os << "(" << dateStr <<
+ ", g" << study.grade << ", e" << study.easiness << ", " <<
+ "i" << study.interval << ")";
+ }
+ else
+ os << "(New card)";
+ return os;
+}
+
+// Create "new" study record
+StudyRecord::StudyRecord():
+ date(QDateTime()),
+ level(New),
+ interval(0),
+ grade(Unknown),
+ easiness(StudySettings::inst()->initEasiness),
+ recallTime(0),
+ answerTime(0)
+ {
+ }
+
+StudyRecord::StudyRecord(int level, int grade, double easiness, double interval):
+ date(QDateTime()),
+ recallTime(0),
+ answerTime(0)
+{
+ this->level = level;
+ this->interval = interval;
+ this->grade = grade;
+ this->easiness = easiness;
+}
+
+bool StudyRecord::operator==(const StudyRecord& aOther) const
+ {
+ return level == aOther.level &&
+ date == aOther.date &&
+ interval == aOther.interval &&
+ grade == aOther.grade &&
+ easiness == aOther.easiness;
+ }
+
+bool StudyRecord::timeTriggered() const
+{
+ if(date.isNull())
+ return false;
+ QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) );
+ return nextRepetition <= TimeProvider::get();
+}
+
+int StudyRecord::getSecsToNextRepetition() const
+{
+ if(date.isNull())
+ return 0;
+ QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) );
+ return TimeProvider::get().secsTo(nextRepetition);
+}
+
+bool StudyRecord::isOneDayOld() const
+{
+ return date.secsTo(TimeProvider::get()) / (60*60*24.) >=
+ StudySettings::inst()->nextDayInterval;
+}
+
+bool StudyRecord::isLearning() const
+{
+ return (level == ShortLearning || level == LongLearning) &&
+ !isOneDayOld();
+}
+
+int StudyRecord::getScheduledTodayReviews() const
+{
+ switch(level)
+ {
+ case New:
+ return 3;
+ case ShortLearning:
+ return 2;
+ case LongLearning:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+bool StudyRecord::isReviewedToday() const
+{
+ QDateTime recShiftedDate = shiftedDate(date);
+ QDateTime curShiftedDate = shiftedDate(QDateTime::currentDateTime());
+ return recShiftedDate.date() == curShiftedDate.date();
+}
+
+bool StudyRecord::isActivatedToday() const
+{
+ if(date.isNull())
+ return false;
+ QDateTime nextRepetition = date.addSecs( (int)(interval * 60*60*24) );
+ QDateTime shiftedNextRep = shiftedDate(nextRepetition);
+ QDateTime curShiftedDate = shiftedDate(QDateTime::currentDateTime());
+ return shiftedNextRep.date() <= curShiftedDate.date();
+}
+
+QDateTime StudyRecord::shiftedDate(const QDateTime& aDate)
+{
+ return aDate.addSecs(-60 * 60 * StudySettings::inst()->dayShift);
+}
+
+void StudyRecord::setRecallTime(double time)
+{
+ recallTime = time;
+ if(recallTime > MaxAnswerTime)
+ recallTime = MaxAnswerTime;
+}
+
+void StudyRecord::setAnswerTime(double time)
+{
+ answerTime = time;
+ if(answerTime > MaxAnswerTime)
+ answerTime = MaxAnswerTime;
+}
diff --git a/src/study/StudyRecord.h b/src/study/StudyRecord.h
new file mode 100644
index 0000000..abe7ef9
--- /dev/null
+++ b/src/study/StudyRecord.h
@@ -0,0 +1,62 @@
+#ifndef STUDYRECORD_H
+#define STUDYRECORD_H
+
+#include <iostream>
+#include <QDateTime>
+
+using std::ostream;
+
+class ScheduleParams;
+
+struct StudyRecord
+{
+public:
+ static const int MaxAnswerTime = 180; // sec
+ enum Level
+ {
+ New = 1,
+ ShortLearning = 2,
+ LongLearning = 3,
+ Repeating = 10
+ };
+ enum Grade
+ {
+ Unknown = 1,
+ Incorrect = 2,
+ Difficult = 3,
+ Good = 4,
+ Easy = 5,
+ GradesNum = 5
+ };
+
+public:
+ StudyRecord();
+ StudyRecord(int level, int grade, double easiness, double interval);
+
+ bool timeTriggered() const;
+ int getSecsToNextRepetition() const;
+ bool isOneDayOld() const;
+ bool isLearning() const;
+ int getScheduledTodayReviews() const;
+ bool isReviewedToday() const;
+ bool isActivatedToday() const;
+ bool operator==( const StudyRecord& aOther ) const;
+ void setRecallTime(double time);
+ void setAnswerTime(double time);
+
+private:
+ static QDateTime shiftedDate(const QDateTime& aDate);
+
+public:
+ QDateTime date;
+ int level;
+ double interval; // days
+ int grade;
+ double easiness;
+ double recallTime; // sec
+ double answerTime; // Full answer time: recall + grading
+};
+
+ostream& operator<<(ostream& os, const StudyRecord& study);
+
+#endif
diff --git a/src/study/StudySettings.cpp b/src/study/StudySettings.cpp
new file mode 100644
index 0000000..c680f8e
--- /dev/null
+++ b/src/study/StudySettings.cpp
@@ -0,0 +1,93 @@
+#include "StudySettings.h"
+
+#include <QCoreApplication>
+#include <QStringList>
+#include <QtDebug>
+
+StudySettings::StudySettings()
+{
+ initDefaultStudy();
+}
+
+void StudySettings::initDefaultStudy()
+{
+ showRandomly = true;
+ newCardsShare = 0.2;
+ schedRandomness = 0.1;
+ cardsDayLimit = 80;
+ newCardsDayLimit = 10;
+ limitForAddingNewCards = 70;
+ dayShift = 3;
+
+ initEasiness = 2.5;
+ minEasiness = 1.3;
+ maxEasiness = 3.2;
+ difficultDelta = -0.14;
+ easyDelta = 0.1;
+
+ unknownInterval = 20./(24*60*60);
+ incorrectInterval = 1./(24*60);
+ learningInterval = 10./(24*60);
+ nextDayInterval = 0.9;
+ twoDaysInterval = 1.9;
+}
+
+StudySettings* StudySettings::inst()
+ {
+ static StudySettings instance;
+ return &instance;
+ }
+
+void StudySettings::load()
+ {
+ QSettings settings;
+ settings.beginGroup("Study");
+ loadStudy(settings);
+ settings.endGroup();
+ }
+
+void StudySettings::loadStudy(const QSettings& settings)
+{
+ showRandomly = settings.value("random", showRandomly).toBool();
+ newCardsShare = settings.value("new-cards-share", newCardsShare).toDouble();
+ schedRandomness =
+ settings.value("scheduling-randomness", schedRandomness).toDouble();
+ cardsDayLimit = settings.value("cards-daylimit", cardsDayLimit).toInt();
+ newCardsDayLimit = settings.value("new-cards-daylimit", newCardsDayLimit).toInt();
+ limitForAddingNewCards = settings.value("limit-for-adding-new-cards", limitForAddingNewCards).toInt();
+ dayShift = settings.value("dayshift", dayShift).toInt();
+
+ initEasiness = settings.value("init-easiness", initEasiness).toDouble();
+ minEasiness = settings.value("min-easiness", minEasiness).toDouble();
+ maxEasiness = settings.value("max-easiness", maxEasiness).toDouble();
+ difficultDelta = settings.value("difficult-delta", difficultDelta).toDouble();
+ easyDelta = settings.value("easy-delta", easyDelta).toDouble();
+ unknownInterval = settings.value("unknown-interval", unknownInterval).toDouble();
+ incorrectInterval = settings.value("incorrect-interval", incorrectInterval).toDouble();
+ learningInterval = settings.value("learning-interval", learningInterval).toDouble();
+ nextDayInterval = settings.value("next-day-interval", nextDayInterval).toDouble();
+}
+
+void StudySettings::save()
+ {
+ QSettings settings;
+ settings.beginGroup("Study");
+ settings.remove(""); // Remove old user settings
+ StudySettings defaults;
+ if(showRandomly != defaults.showRandomly)
+ settings.setValue( "random", showRandomly );
+ if(newCardsShare != defaults.newCardsShare)
+ settings.setValue( "new-cards-share", newCardsShare );
+ if(schedRandomness != defaults.schedRandomness)
+ settings.setValue( "scheduling-randomness", schedRandomness );
+ if(cardsDayLimit != defaults.cardsDayLimit)
+ settings.setValue( "cards-daylimit",cardsDayLimit );
+ if(newCardsDayLimit != defaults.newCardsDayLimit)
+ settings.setValue( "new-cards-daylimit", newCardsDayLimit );
+ if(limitForAddingNewCards != defaults.limitForAddingNewCards)
+ settings.setValue( "limit-for-adding-new-cards", limitForAddingNewCards );
+ if(dayShift != defaults.dayShift)
+ settings.setValue( "dayshift", dayShift );
+ settings.endGroup();
+}
+
diff --git a/src/study/StudySettings.h b/src/study/StudySettings.h
new file mode 100644
index 0000000..d65e8a7
--- /dev/null
+++ b/src/study/StudySettings.h
@@ -0,0 +1,40 @@
+#ifndef STUDYSETTINGS_H
+#define STUDYSETTINGS_H
+
+#include <QSettings>
+
+class StudySettings
+{
+public:
+ static StudySettings* inst();
+
+public:
+ StudySettings();
+ void save();
+ void load();
+
+private:
+ void loadUserSettings();
+ void initDefaultStudy();
+ void loadStudy(const QSettings& settings);
+
+public:
+ bool showRandomly;
+ double newCardsShare;
+ double schedRandomness;
+ int cardsDayLimit;
+ int newCardsDayLimit;
+ int limitForAddingNewCards;
+ int dayShift; // in hours
+ double initEasiness;
+ double minEasiness;
+ double maxEasiness;
+ double difficultDelta;
+ double easyDelta;
+ double unknownInterval;
+ double incorrectInterval;
+ double learningInterval; // Long learning level
+ double nextDayInterval; // first repetition
+ double twoDaysInterval; // easy first repetition
+};
+#endif
diff --git a/src/study/WarningPanel.cpp b/src/study/WarningPanel.cpp
new file mode 100644
index 0000000..9bf6122
--- /dev/null
+++ b/src/study/WarningPanel.cpp
@@ -0,0 +1,49 @@
+#include "WarningPanel.h"
+
+WarningPanel::WarningPanel(QWidget* parent):
+ QFrame(parent),
+ warningLabel(new QLabel)
+{
+ initPanel();
+ setLayout(createWarningLayout());
+}
+
+void WarningPanel::initPanel()
+{
+ setFrameStyle(QFrame::StyledPanel);
+ setPalette(QPalette("#ffffcc"));
+ setAutoFillBackground(true);
+ hide();
+}
+
+QHBoxLayout* WarningPanel::createWarningLayout()
+{
+ QHBoxLayout* warningLt = new QHBoxLayout;
+ warningLt->addWidget(createWarningIconLabel(), 0, Qt::AlignTop);
+ warningLt->addWidget(warningLabel, 1);
+ warningLt->addWidget(createWarningCloseButton(), 0, Qt::AlignTop);
+ return warningLt;
+}
+
+void WarningPanel::setText(const QString& text)
+{
+ warningLabel->setText(text);
+}
+
+QLabel* WarningPanel::createWarningIconLabel() const
+{
+ QLabel* label = new QLabel;
+ label->setPixmap( QPixmap(":/images/warning.png").scaled(
+ 24, 24, Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
+ return label;
+}
+
+QToolButton* WarningPanel::createWarningCloseButton() const
+{
+ QToolButton* button = new QToolButton;
+ button->setAutoRaise(true);
+ button->setIcon(QIcon(":/images/gray-cross.png"));
+ button->setShortcut(Qt::Key_Escape);
+ connect(button, SIGNAL(clicked()), this, SLOT(hide()));
+ return button;
+}
diff --git a/src/study/WarningPanel.h b/src/study/WarningPanel.h
new file mode 100644
index 0000000..f207843
--- /dev/null
+++ b/src/study/WarningPanel.h
@@ -0,0 +1,25 @@
+#ifndef WARNING_PANEL_H
+#define WARNING_PANEL_H
+
+#include <QtWidgets>
+
+class WarningPanel: public QFrame
+{
+ Q_OBJECT
+
+public:
+ WarningPanel(QWidget* parent = 0);
+ void setText(const QString& text);
+
+private:
+ QLabel* createWarningIconLabel() const;
+ QToolButton* createWarningCloseButton() const;
+ QHBoxLayout* createWarningLayout();
+ void initPanel();
+
+private:
+ QLabel* warningLabel;
+
+};
+
+#endif
diff --git a/src/study/WordDrillModel.cpp b/src/study/WordDrillModel.cpp
new file mode 100644
index 0000000..403535a
--- /dev/null
+++ b/src/study/WordDrillModel.cpp
@@ -0,0 +1,163 @@
+#include "WordDrillModel.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+#include "StudySettings.h"
+
+#include <stdlib.h>
+#include <time.h>
+
+WordDrillModel::WordDrillModel( CardPack* aCardPack ):
+ IStudyModel( aCardPack ), m_historyCurPackStart(0)
+ {
+ curCardNum = NoCardIndex;
+ srand( time(NULL) );
+ connect( aCardPack, SIGNAL(cardsGenerated()), SLOT(updateStudyState()) );
+
+ generateFreshPack();
+ pickNextCard();
+ }
+
+/** Get current card.
+ * If the current card doesn't exist any more, find the next valid card.
+ */
+
+Card* WordDrillModel::getCurCard() const
+ {
+ if( m_cardHistory.isEmpty() || curCardNum < 0 ) // No cards in the pack
+ return NULL;
+ if( curCardNum >= m_cardHistory.size() )
+ return NULL;
+ QString cardName = m_cardHistory.at(curCardNum);
+ return cardPack->getCard( cardName );
+ }
+
+void WordDrillModel::updateStudyState()
+ {
+ generateFreshPack();
+ cleanHistoryFromRemovedCards();
+ if( getCurCard() )
+ emit curCardUpdated();
+ else
+ pickNextCard();
+ }
+
+void WordDrillModel::generateFreshPack()
+ {
+ m_freshPack = cardPack->getCardQuestions();
+ // Remove already reviewed cards
+ if( m_historyCurPackStart >= m_cardHistory.size() )
+ return;
+ foreach( QString reviewedCard, m_cardHistory.mid( m_historyCurPackStart ) )
+ m_freshPack.removeAll( reviewedCard );
+ }
+
+void WordDrillModel::cleanHistoryFromRemovedCards()
+ {
+ if( m_cardHistory.isEmpty() )
+ return;
+ bool cardsRemoved = false;
+ QMutableStringListIterator it( m_cardHistory );
+ int i = 0;
+ while( it.hasNext() )
+ {
+ QString cardName = it.next();
+ if( !cardPack->containsQuestion( cardName ) )
+ {
+ it.remove();
+ cardsRemoved = true;
+ if( i < curCardNum )
+ curCardNum--;
+ }
+ i++;
+ }
+ if( cardsRemoved )
+ {
+ if( curCardNum >= m_cardHistory.size() )
+ curCardNum = m_cardHistory.size() - 1;
+ emit nextCardSelected();
+ }
+ }
+
+
+/** Picks a random card. Removes the selected card from the fresh pack and adds it to the history.
+ * Thus, the new card is the last entry in the history.
+ * This function guarantees that the new card's question will be different from the previous one, unless there is no choice.
+ *
+ * Updates #m_curCardNum.
+ */
+void WordDrillModel::pickNextCard()
+ {
+ QString selectedCardName;
+ const Card* selectedCard = NULL;
+ do
+ {
+ if( m_freshPack.isEmpty() ) // No fresh cards
+ {
+ m_historyCurPackStart = m_cardHistory.size(); // Refers beyond the history pack
+ generateFreshPack();
+ if( m_freshPack.isEmpty() ) // Still no any cards available - no useful cards in the dictionary or it's empty
+ {
+ curCardNum = NoCardIndex;
+ emit nextCardSelected();
+ return;
+ }
+ }
+ if( m_freshPack.size() == 1 ) // Only 1 fresh card, no choice
+ selectedCardName = m_freshPack.takeFirst();
+ else
+ {
+ int selectedCardNum;
+ if( StudySettings::inst()->showRandomly )
+ selectedCardNum = rand() % m_freshPack.size();
+ else
+ selectedCardNum = 0;
+ if( !m_cardHistory.isEmpty() )
+ while( m_freshPack[ selectedCardNum ] == m_cardHistory.last() ) // The new question must be different from the current one
+ {
+ selectedCardNum++;
+ selectedCardNum %= m_freshPack.size();
+ }
+ selectedCardName = m_freshPack.takeAt( selectedCardNum );
+ }
+ selectedCard = cardPack->getCard( selectedCardName );
+ }
+ while( !selectedCard );
+ m_cardHistory << selectedCardName;
+ curCardNum = m_cardHistory.size() - 1;
+ emit nextCardSelected();
+ }
+
+/** Go back along the history line.
+ * @return true, if the transition was successful
+ */
+bool WordDrillModel::goBack()
+ {
+ if( !canGoBack() )
+ return false;
+ curCardNum--;
+ emit nextCardSelected();
+ return true;
+ }
+
+/** Go forward along the history line.
+ * @return true, if the transition was successful
+ */
+bool WordDrillModel::goForward()
+ {
+ if( !canGoForward() )
+ return false;
+ curCardNum++;
+ emit nextCardSelected();
+ return true;
+ }
+
+bool WordDrillModel::canGoBack()
+ {
+ return curCardNum > 0;
+ }
+
+bool WordDrillModel::canGoForward()
+ {
+ return curCardNum < m_cardHistory.size() - 1;
+ }
+
diff --git a/src/study/WordDrillModel.h b/src/study/WordDrillModel.h
new file mode 100644
index 0000000..7db780a
--- /dev/null
+++ b/src/study/WordDrillModel.h
@@ -0,0 +1,62 @@
+#ifndef WORDDRILLMODEL_H
+#define WORDDRILLMODEL_H
+
+#include "IStudyModel.h"
+
+#include <QStringList>
+
+/** Model of Word Drill study tool.
+ *
+ * At the beginning of a test, the model generates a fresh pack of valid cards: #m_freshPack. In course of the test,
+ * cards one-by-one are randomly taken from the fresh pack and moved to the history pack #m_cardHistory.
+ * The part of history, which belongs to the current pack (recently generated new pack), is separated from older
+ * cards with property #m_historyCurPackStart.
+ * The current card can be obtained with #curCard(), it is taken from the history. When the fresh pack is finished
+ * and the next card required, new fresh pack is generated.
+ *
+ * All used cards over all pack iterations are saved to the history.
+ * The user can travel back and forth along the history with goBack() and goForward(). In this case #m_curCardNum
+ * increases or decreases.
+ */
+
+class WordDrillModel: public IStudyModel
+{
+Q_OBJECT
+
+public:
+ WordDrillModel( CardPack* aCardPack );
+
+ Card* getCurCard() const;
+ bool canGoBack();
+ bool canGoForward();
+
+ int getCurCardNum() const { return curCardNum; }
+ int historySize() const {return m_cardHistory.size(); }
+
+private:
+ void generateFreshPack();
+ void cleanHistoryFromRemovedCards();
+
+public slots:
+ void pickNextCard();
+ bool goBack();
+ bool goForward();
+
+private slots:
+ void updateStudyState();
+
+private:
+ static const int NoCardIndex = -1;
+
+private:
+ /** Fresh cards. The cards that were not shown yet (in the current iteration). Not own.*/
+ QStringList m_freshPack;
+
+ /// List of all shown cards. Not own.
+ QStringList m_cardHistory;
+
+ /// The start index of the current pack in the history
+ int m_historyCurPackStart;
+};
+
+#endif
diff --git a/src/study/WordDrillWindow.cpp b/src/study/WordDrillWindow.cpp
new file mode 100644
index 0000000..dd13bd1
--- /dev/null
+++ b/src/study/WordDrillWindow.cpp
@@ -0,0 +1,180 @@
+#include "WordDrillWindow.h"
+#include "WordDrillModel.h"
+#include "../dictionary/Card.h"
+#include "../dictionary/CardPack.h"
+#include "CardSideView.h"
+
+WordDrillWindow::WordDrillWindow( WordDrillModel* aModel, QWidget* aParent ):
+ IStudyWindow(aModel, tr("Word drill"), aParent)
+{
+ studyType = AppModel::WordDrill;
+ createUI(); // Can't call from parent constructor
+ setWindowIcon( QIcon(":/images/word-drill.png") );
+ showNextCard();
+}
+
+WordDrillWindow::~WordDrillWindow()
+{
+ WriteSettings();
+}
+
+QVBoxLayout* WordDrillWindow::createLowerPanel()
+{
+ QHBoxLayout* cardNumLayout = new QHBoxLayout;
+ iCardNumLabel = new QLabel;
+ iCardNumLabel->setAlignment( Qt::AlignVCenter );
+ iCardNumLabel->setToolTip(tr("Current card / All cards"));
+ iProgressBar = new QProgressBar;
+ iProgressBar->setTextVisible( false );
+ iProgressBar->setMaximumHeight( iProgressBar->sizeHint().height()/2 );
+ iProgressBar->setToolTip(tr("Progress of reviewing cards"));
+ iShowAnswersCB = new QCheckBox(tr("Show answers"));
+ iShowAnswersCB->setShortcut( tr("S", "Shortcut for 'Show answers' checkbox") );
+ iShowAnswersCB->setChecked( true );
+ connect( iShowAnswersCB, SIGNAL(stateChanged(int)), this, SLOT(ToggleAnswer()) );
+
+ cardNumLayout->addWidget( iCardNumLabel );
+ cardNumLayout->addWidget( iProgressBar );
+ cardNumLayout->addWidget( iShowAnswersCB );
+
+ iBackBtn = new QPushButton( QIcon(":/images/back.png"), tr("Back"), this);
+ iBackBtn->setShortcut( Qt::Key_Left );
+ iBackBtn->setToolTip( tr("Go back in history") + ShortcutToStr( iBackBtn ) );
+ connect( iBackBtn, SIGNAL(clicked()), qobject_cast<WordDrillModel*>(m_model), SLOT(goBack()) );
+
+ iForwardBtn = new QPushButton( QIcon(":/images/forward.png"), tr("Forward"), this);
+ iForwardBtn->setShortcut( Qt::Key_Right );
+ iForwardBtn->setToolTip( tr("Go forward in history") + ShortcutToStr( iForwardBtn ) );
+ connect( iForwardBtn, SIGNAL(clicked()), qobject_cast<WordDrillModel*>(m_model), SLOT(goForward()) );
+
+ iNextBtn = new QPushButton( QIcon(":/images/next.png"), tr("Next"), this);
+ iNextBtn->setShortcut( Qt::Key_Return );
+ iNextBtn->setToolTip( tr("Show next card (Enter)") );
+ connect( iNextBtn, SIGNAL(clicked()), qobject_cast<WordDrillModel*>(m_model), SLOT(pickNextCard()) );
+
+ QHBoxLayout* controlLt = new QHBoxLayout;
+ controlLt->addWidget( iBackBtn );
+ controlLt->addWidget( iForwardBtn );
+ controlLt->addWidget( iNextBtn );
+
+ QVBoxLayout* lowerPanelLt = new QVBoxLayout;
+ lowerPanelLt->addLayout( cardNumLayout );
+ if( bigScreen() )
+ lowerPanelLt->addLayout( controlLt );
+
+ return lowerPanelLt;
+}
+
+void WordDrillWindow::setStateForNextCard()
+{
+ if( m_model->getCurCard() )
+ state = iShowAnswersCB->isChecked()? StateAnswerVisible: StateAnswerHidden;
+ else
+ state = StateNoCards;
+ }
+
+void WordDrillWindow::processState()
+{
+if( state == StateAnswerHidden || state == StateAnswerVisible )
+ {
+ centralStackedLt->setCurrentIndex( CardPage );
+ DisplayCardNum();
+ UpdateButtons();
+ }
+switch( state )
+ {
+ case StateAnswerHidden:
+ questionLabel->setQuestion( m_model->getCurCard()->getQuestion() );
+ answerStackedLt->setCurrentIndex( AnsButtonPage );
+ answerBtn->setFocus();
+ break;
+ case StateAnswerVisible:
+ {
+ Card* card = m_model->getCurCard();
+ if( iShowAnswersCB->isChecked() ) // StateAnswerHidden was skipped
+ questionLabel->setQuestion( card->getQuestion() );
+ answerStackedLt->setCurrentIndex( AnsLabelPage );
+ answerLabel->setQstAnsr( card->getQuestion(), card->getAnswers() );
+ iNextBtn->setFocus();
+ break;
+ }
+ case StateNoCards:
+ centralStackedLt->setCurrentIndex(MessagePage);
+ messageLabel->setText(
+ QString("<div align=\"center\" style=\"font-size: 22pt; font-weight: bold\">") +
+ tr("No cards available") + "</div>");
+ break;
+ }
+}
+
+void WordDrillWindow::DisplayCardNum()
+{
+int curCardIndex = qobject_cast<WordDrillModel*>(m_model)->getCurCardNum();
+int curCardNum = curCardIndex + 1;
+int historySize = qobject_cast<WordDrillModel*>(m_model)->historySize();
+int packSize = m_model->getCardPack()->cardsNum();
+
+int curCardNumInPack = curCardIndex % packSize + 1;
+int passNum;
+if( packSize > 0 )
+ passNum = curCardIndex / packSize + 1;
+else
+ passNum = 1;
+QString cardNumStr = QString("%1/%2").arg( curCardNumInPack ).arg( packSize );
+QString passIconStr = "<img src=\":/images/passes.png\"/>";
+if( passNum > 1 )
+ cardNumStr += " " + passIconStr + QString::number( passNum );
+if( curCardNum < historySize && packSize > 0 )
+ {
+ int lastCardIndex = historySize - 1;
+ int lastCardNumInPack = lastCardIndex % packSize + 1;
+ cardNumStr += " (" + QString::number( lastCardNumInPack );
+ int sessionPassNum = lastCardIndex / packSize + 1;
+ if( sessionPassNum > 1 )
+ cardNumStr += " " + passIconStr + QString::number( sessionPassNum );
+ cardNumStr += ")";
+ }
+
+iCardNumLabel->setText( cardNumStr );
+iProgressBar->setMaximum( packSize );
+iProgressBar->setValue( curCardNumInPack );
+}
+
+void WordDrillWindow::UpdateButtons()
+{
+iBackBtn->setEnabled( qobject_cast<WordDrillModel*>(m_model)->canGoBack() );
+iForwardBtn->setEnabled( qobject_cast<WordDrillModel*>(m_model)->canGoForward() );
+}
+
+void WordDrillWindow::ToggleAnswer()
+{
+if( !m_model->getCurCard() )
+ return;
+answerStackedLt->setCurrentIndex( iShowAnswersCB->isChecked()? AnsLabelPage: AnsButtonPage );
+switch( state )
+ {
+ case StateAnswerHidden:
+ state = StateAnswerVisible;
+ break;
+ case StateAnswerVisible:
+ state = iShowAnswersCB->isChecked()? StateAnswerVisible: StateAnswerHidden;
+ break;
+ default:
+ return;
+ }
+processState();
+}
+
+void WordDrillWindow::ReadSettings()
+{
+QSettings settings;
+move( settings.value("worddrill-pos", QPoint(PosX, PosY)).toPoint() );
+resize( settings.value("worddrill-size", QSize(Width, Height)).toSize() );
+}
+
+void WordDrillWindow::WriteSettings()
+{
+QSettings settings;
+settings.setValue("worddrill-pos", pos());
+settings.setValue("worddrill-size", size());
+}
diff --git a/src/study/WordDrillWindow.h b/src/study/WordDrillWindow.h
new file mode 100644
index 0000000..ed421e9
--- /dev/null
+++ b/src/study/WordDrillWindow.h
@@ -0,0 +1,46 @@
+#ifndef WORDDRILLWINDOW_H
+#define WORDDRILLWINDOW_H
+
+#include <QtWidgets>
+#include "IStudyWindow.h"
+
+class WordDrillModel;
+
+class WordDrillWindow: public IStudyWindow
+{
+ Q_OBJECT
+
+public:
+ WordDrillWindow( WordDrillModel* aModel, QWidget* aParent );
+ ~WordDrillWindow();
+
+protected:
+ QVBoxLayout* createLowerPanel();
+ void setStateForNextCard();
+ void processState();
+ void ReadSettings();
+ void WriteSettings();
+
+private:
+ void DisplayCardNum();
+ void UpdateButtons();
+
+private slots:
+ void ToggleAnswer();
+
+private:
+ static const int PosX = 200;
+ static const int PosY = 200;
+ static const int Width = 600;
+ static const int Height = 350;
+
+private:
+ QLabel* iCardNumLabel;
+ QProgressBar* iProgressBar;
+ QCheckBox* iShowAnswersCB;
+ QPushButton* iBackBtn;
+ QPushButton* iForwardBtn;
+ QPushButton* iNextBtn;
+};
+
+#endif
diff --git a/src/utils/IRandomGenerator.h b/src/utils/IRandomGenerator.h
new file mode 100644
index 0000000..08c6f95
--- /dev/null
+++ b/src/utils/IRandomGenerator.h
@@ -0,0 +1,17 @@
+#ifndef I_RANDOM_GENERATOR_H
+#define I_RANDOM_GENERATOR_H
+
+#include <QtCore>
+
+class IRandomGenerator
+{
+public:
+ virtual ~IRandomGenerator() {}
+ virtual double getInRange_11() const = 0;
+ virtual double getInRange_01() const = 0;
+ virtual int getRand() const = 0;
+ virtual int getRand(int maxNum) const = 0;
+ virtual QByteArray getArray() const = 0;
+};
+
+#endif
diff --git a/src/utils/RandomGenerator.cpp b/src/utils/RandomGenerator.cpp
new file mode 100644
index 0000000..9ffe4fb
--- /dev/null
+++ b/src/utils/RandomGenerator.cpp
@@ -0,0 +1,38 @@
+#include "RandomGenerator.h"
+
+#include <stdlib.h>
+#include <time.h>
+
+RandomGenerator::RandomGenerator()
+{
+ srand(time(NULL));
+}
+
+double RandomGenerator::getInRange_11() const
+{
+ return 2. * rand() / RAND_MAX - 1;
+}
+
+double RandomGenerator::getInRange_01() const
+{
+ return float(rand()) / RAND_MAX;
+}
+
+int RandomGenerator::getRand() const
+{
+ return rand();
+}
+
+int RandomGenerator::getRand(int maxNum) const
+{
+ return rand() % maxNum;
+}
+
+QByteArray RandomGenerator::getArray() const
+{
+ const int size = 16;
+ QByteArray res;
+ for(int i = 0; i < size; i++)
+ res += rand() % 256;
+ return res;
+}
diff --git a/src/utils/RandomGenerator.h b/src/utils/RandomGenerator.h
new file mode 100644
index 0000000..da36070
--- /dev/null
+++ b/src/utils/RandomGenerator.h
@@ -0,0 +1,16 @@
+#ifndef RANDOM_GENERATOR_H
+#define RANDOM_GENERATOR_H
+
+#include "IRandomGenerator.h"
+
+class RandomGenerator: public IRandomGenerator
+{
+public:
+ RandomGenerator();
+ double getInRange_11() const;
+ double getInRange_01() const;
+ int getRand() const;
+ int getRand(int maxNum) const;
+ QByteArray getArray() const;
+};
+#endif
diff --git a/src/utils/TimeProvider.cpp b/src/utils/TimeProvider.cpp
new file mode 100644
index 0000000..db24c5e
--- /dev/null
+++ b/src/utils/TimeProvider.cpp
@@ -0,0 +1,6 @@
+#include "TimeProvider.h"
+
+QDateTime TimeProvider::get()
+{
+ return QDateTime::currentDateTime();
+}
diff --git a/src/utils/TimeProvider.h b/src/utils/TimeProvider.h
new file mode 100644
index 0000000..697d8eb
--- /dev/null
+++ b/src/utils/TimeProvider.h
@@ -0,0 +1,12 @@
+#ifndef TIME_PROVIDER_H
+#define TIME_PROVIDER_H
+
+#include <QtCore>
+
+class TimeProvider
+{
+public:
+ static QDateTime get();
+};
+
+#endif
diff --git a/src/version.cpp b/src/version.cpp
new file mode 100644
index 0000000..4ee51bf
--- /dev/null
+++ b/src/version.cpp
@@ -0,0 +1,3 @@
+#include "version.h"
+
+QString BuildStr;
diff --git a/src/version.h b/src/version.h
new file mode 100644
index 0000000..526a65c
--- /dev/null
+++ b/src/version.h
@@ -0,0 +1,19 @@
+#ifndef VERSION_H
+#define VERSION_H
+
+#include <QString>
+
+#define DIC_VERSION "1.4" // Version of the dictionary XML file
+#define STUDY_VERSION "1.4" // Version of the study file
+
+#ifndef FM_VERSION
+ #define FM_VERSION "0.0"
+#endif
+
+#ifndef BUILD_REVISION
+ #define BUILD_REVISION ""
+#endif
+
+extern QString BuildStr;
+
+#endif
diff --git a/tests/common/RecordsParam.cpp b/tests/common/RecordsParam.cpp
new file mode 100644
index 0000000..74ad802
--- /dev/null
+++ b/tests/common/RecordsParam.cpp
@@ -0,0 +1,51 @@
+#include "RecordsParam.h"
+
+#include "../../src/dictionary/Field.h"
+#include "../../src/dictionary/DicRecord.h"
+
+const vector<string> RecordsParam::fieldNames {"English", "Russian", "Finnish"};
+
+RecordsParam::RecordsParam(
+ vector<int> packFields,
+ vector<vector<string> > records,
+ vector<string> questions,
+ vector<vector<string> > answers):
+ packFields(packFields),
+ questions(questions),
+ answers(answers)
+{
+ for(vector<string> fieldValues: records)
+ {
+ DicRecord* record = new DicRecord;
+ for(unsigned i = 0; i < fieldValues.size(); i++)
+ record->setField(fieldNames[i].c_str(), fieldValues[i].c_str());
+ this->records << record;
+ }
+}
+
+vector<string> RecordsParam::hashToStrVector(const QHash<QString, QString>& hash,
+ const vector<string>& keys)
+{
+ vector<string> res;
+ for(unsigned i = 0; i < keys.size(); i++)
+ res.push_back( hash[keys[i].c_str()].toStdString() );
+ return res;
+}
+
+vector<string> RecordsParam::recordsToStr() const
+{
+ vector<string> res;
+ for(DicRecord* record: records)
+ {
+ vector<string> fieldValues = hashToStrVector(record->getFields(), fieldNames);
+ res.push_back(string("(") + vectorToStr<string>(fieldValues) + ")");
+ }
+ return res;
+}
+
+ostream& operator<<(ostream& os, const RecordsParam& param)
+{
+ os << "Fields(" << param.vectorToStr<int>(param.packFields) << ") ";
+ os << "{" << param.vectorToStr<string>( param.recordsToStr() ) << "}";
+ return os;
+}
diff --git a/tests/common/RecordsParam.h b/tests/common/RecordsParam.h
new file mode 100644
index 0000000..19d4532
--- /dev/null
+++ b/tests/common/RecordsParam.h
@@ -0,0 +1,58 @@
+#ifndef RECORDS_PARAM_H
+#define RECORDS_PARAM_H
+
+#include <iostream>
+#include <vector>
+#include <string>
+#include <sstream>
+#include <QtCore>
+
+class DicRecord;
+
+using std::vector;
+using std::string;
+using std::ostream;
+
+struct RecordsParam
+{
+public:
+ static vector<RecordsParam> createParams();
+
+ template <typename T>
+ static string vectorToStr(const vector<T>& v);
+
+ static vector<string> hashToStrVector(const QHash<QString, QString>& hash,
+ const vector<string>& keys);
+
+public:
+ RecordsParam(vector<int> packFields, vector<vector<string> > records,
+ vector<string> questions, vector<vector<string> > answers);
+
+ vector<string> recordsToStr() const;
+
+public:
+ static const vector<string> fieldNames;
+
+public:
+ QList<DicRecord*> records;
+ vector<int> packFields;
+ vector<string> questions;
+ vector<vector<string> > answers;
+};
+
+ostream& operator<<(ostream& os, const RecordsParam& param);
+
+template <typename T>
+string RecordsParam::vectorToStr(const vector<T>& v)
+{
+ std::stringstream ss;
+ for(unsigned i = 0; i < v.size(); i++)
+ {
+ if(i != 0)
+ ss << ", ";
+ ss << v[i];
+ }
+ return ss.str();
+}
+
+#endif
diff --git a/tests/common/RecordsParam_create.cpp b/tests/common/RecordsParam_create.cpp
new file mode 100644
index 0000000..b247f35
--- /dev/null
+++ b/tests/common/RecordsParam_create.cpp
@@ -0,0 +1,62 @@
+#include "RecordsParam.h"
+
+typedef RecordsParam RP;
+
+vector<RecordsParam> RecordsParam::createParams()
+{
+return
+ {
+ RP({0, 1},
+ {{"table", "стол"}, {"window", "окно"}},
+ {"table", "window"},
+ {{"table", "стол"}, {"window", "окно"}}),
+ RP({0, 1},
+ {{"table", "стол"}, {"window", "окно"}, {"world", "мир"}},
+ {"table", "window", "world"},
+ {{"table", "стол"}, {"window", "окно"}, {"world", "мир"}}),
+ RP({1, 0},
+ {{"table", "стол"}, {"window", "окно"}},
+ {"стол", "окно"},
+ {{"стол", "table"}, {"окно", "window"}}),
+ RP({0, 1},
+ {{"table", "стол"}, {"table", "стол"}},
+ {"table"},
+ {{"table", "стол"}}),
+ RP({0, 1},
+ {{"table", "стол"}, {"table", "таблица"}},
+ {"table"},
+ {{"table", "стол; таблица"}}),
+ RP({1, 0},
+ {{"world", "мир"}, {"peace", "мир"}},
+ {"мир"},
+ {{"мир", "world; peace"}}),
+ RP({1, 0},
+ {{"man", "человек; мужчина"}, {"man", "мужик"}},
+ {"человек", "мужчина", "мужик"},
+ {{"человек", "man"}, {"мужчина", "man"}, {"мужик", "man"}}),
+ RP({0, 1},
+ {{"better; best", "лучший"}},
+ {"better", "best"},
+ {{"better", "лучший"}, {"best", "лучший"}}),
+ RP({0, 1},
+ {{"table", ""},},
+ {},
+ {}),
+ RP({0, 1},
+ {{"", "стол"},},
+ {},
+ {}),
+ RP({0, 1, 2},
+ {{"table", "стол", "pöytä"}, {"window", "окно", "ikkuna"}},
+ {"table", "window"},
+ {{"table", "стол", "pöytä"}, {"window", "окно", "ikkuna"}}),
+ RP({0, 2, 1},
+ {{"table", "стол", "pöytä"}, {"window", "окно", "ikkuna"}},
+ {"table", "window"},
+ {{"table", "pöytä", "стол"}, {"window", "ikkuna", "окно"}}),
+ RP({0, 1, 2},
+ {{"table", "", "pöytä"}, {"window", "окно", ""}},
+ {"table", "window"},
+ {{"table", "", "pöytä"}, {"window", "окно", ""}}),
+ };
+}
diff --git a/tests/common/printQtTypes.cpp b/tests/common/printQtTypes.cpp
new file mode 100644
index 0000000..72975b7
--- /dev/null
+++ b/tests/common/printQtTypes.cpp
@@ -0,0 +1,31 @@
+#include "printQtTypes.h"
+
+void PrintTo(const QString& str, ::std::ostream* os)
+{
+ *os << "\"" << str.toStdString() << "\"";
+}
+
+void PrintTo(const QStringList& list, ::std::ostream* os)
+{
+ *os << "(" << list.join(", ").toStdString() << ")";
+}
+
+void PrintTo(const QDateTime& time, ::std::ostream* os)
+{
+ *os << time.toString("yyyy-MM-dd HH:mm:ss").toStdString();
+}
+
+void PrintTo(const QByteArray& array, ::std::ostream* os)
+{
+ *os << "\"";
+ const char* hex = array.toHex().constData();
+ for(int i = 0; i < array.size(); i++)
+ {
+ unsigned char ch = array.constData()[i];
+ if(ch >= 32 && ch <= 126)
+ *os << ch;
+ else
+ *os << "\\x" << hex[i * 2] << hex[i * 2 + 1] << " ";
+ }
+ *os << "\"";
+}
diff --git a/tests/common/printQtTypes.h b/tests/common/printQtTypes.h
new file mode 100644
index 0000000..20c23d6
--- /dev/null
+++ b/tests/common/printQtTypes.h
@@ -0,0 +1,23 @@
+#ifndef PRINT_QT_TYPES_H
+#define PRINT_QT_TYPES_H
+
+#include <iostream>
+#include <QtCore>
+#include <vector>
+
+using std::ostream;
+using std::vector;
+
+void PrintTo(const QString& str, ::std::ostream* os);
+void PrintTo(const QStringList& list, ::std::ostream* os);
+
+#define ASSERT_EQ_QSTR(x, y) ASSERT_EQ(x, y) << "\"" << x.toStdString() << "\"" << \
+ " != " << "\"" << y.toStdString() << "\"";
+
+#define ASSERT_EQ_QSTRLIST(x, y) ASSERT_EQ(x, y) << "(" << x.join(", ").toStdString() << ")" << \
+ " != " << "(" << y.join(", ").toStdString() << ")";
+
+void PrintTo(const QDateTime& time, ::std::ostream* os);
+void PrintTo(const QByteArray& array, ::std::ostream* os);
+
+#endif
diff --git a/tests/fute/charts/charts.pro b/tests/fute/charts/charts.pro
new file mode 100644
index 0000000..613fee8
--- /dev/null
+++ b/tests/fute/charts/charts.pro
@@ -0,0 +1,35 @@
+TEMPLATE = app
+QT += widgets
+TARGET = charts_test
+DEPENDPATH += .
+INCLUDEPATH += .
+QMAKE_CXXFLAGS += -std=gnu++11
+DESTDIR = ./
+
+win32: {
+ CONFIG += console
+ }
+
+HEADERS = \
+ charts_test.h \
+ ../../../src/charts/Chart.h \
+ ../../../src/charts/TimeChart.h \
+ ../../../src/charts/DataPoint.h \
+ ../../../src/charts/ChartScene.h \
+ ../../../src/charts/ChartView.h \
+ ../../../src/charts/ChartAxes.h \
+ ../../../src/charts/ChartDataLine.h \
+ ../../../src/charts/ChartMarker.h \
+ ../../../src/charts/ChartToolTip.h
+
+SOURCES = \
+ main.cpp \
+ charts_test.cpp \
+ ../../../src/charts/Chart.cpp \
+ ../../../src/charts/TimeChart.cpp \
+ ../../../src/charts/ChartScene.cpp \
+ ../../../src/charts/ChartView.cpp \
+ ../../../src/charts/ChartAxes.cpp \
+ ../../../src/charts/ChartDataLine.cpp \
+ ../../../src/charts/ChartMarker.cpp \
+ ../../../src/charts/ChartToolTip.cpp
diff --git a/tests/fute/charts/charts_test.cpp b/tests/fute/charts/charts_test.cpp
new file mode 100644
index 0000000..0870027
--- /dev/null
+++ b/tests/fute/charts/charts_test.cpp
@@ -0,0 +1,52 @@
+#include "charts_test.h"
+#include "../../../src/charts/Chart.h"
+
+#include <cstdlib>
+#include <time.h>
+
+ChartsTest::ChartsTest()
+{
+ srand(time(NULL));
+ createUi();
+ changeDataSet();
+}
+
+void ChartsTest::changeDataSet()
+{
+ const int daysNum = 7;
+ yValuesStr = "Values: ";
+ dataSet.clear();
+ for (int i = 0; i < daysNum; i++)
+ addDataPoint(i);
+ chart->setDataSet(dataSet);
+ valuesLabel->setText(yValuesStr);
+}
+
+void ChartsTest::addDataPoint(int index)
+{
+ const int firstDay = 15;
+ QString xLabel = QString::number(firstDay + index) + ".11";
+ int yValue = rand() % 70;
+ dataSet << DataPoint(xLabel, yValue, xLabel);
+ yValuesStr += QString::number(yValue) + ", ";
+}
+
+void ChartsTest::createUi()
+{
+ QPushButton* newBtn = new QPushButton(tr("New chart"));
+ connect(newBtn, SIGNAL(clicked()), SLOT(changeDataSet()));
+
+ valuesLabel = new QLabel;
+ chart = new Chart;
+ chart->setLabels("Date", "Value");
+
+ QHBoxLayout* controlLt = new QHBoxLayout;
+ controlLt->addWidget(valuesLabel);
+ controlLt->addWidget(newBtn);
+
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addLayout(controlLt);
+ mainLt->addWidget(chart);
+ setLayout(mainLt);
+ resize(800, 500);
+}
diff --git a/tests/fute/charts/charts_test.h b/tests/fute/charts/charts_test.h
new file mode 100644
index 0000000..3975444
--- /dev/null
+++ b/tests/fute/charts/charts_test.h
@@ -0,0 +1,31 @@
+#ifndef CHARTS_TEST_H
+#define CHARTS_TEST_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "../../../src/charts/DataPoint.h"
+
+class Chart;
+
+class ChartsTest: public QWidget
+{
+ Q_OBJECT
+public:
+ ChartsTest();
+
+private:
+ void createUi();
+ void addDataPoint(int index);
+
+private slots:
+ void changeDataSet();
+
+private:
+ QString yValuesStr;
+ Chart* chart;
+ QList<DataPoint> dataSet;
+ QLabel* valuesLabel;
+};
+
+#endif
diff --git a/tests/fute/charts/main.cpp b/tests/fute/charts/main.cpp
new file mode 100644
index 0000000..346133d
--- /dev/null
+++ b/tests/fute/charts/main.cpp
@@ -0,0 +1,12 @@
+#include <QtWidgets>
+
+#include "charts_test.h"
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ ChartsTest mainWin;
+ mainWin.show();
+ return app.exec();
+}
+
diff --git a/tests/fute/pieCharts/main.cpp b/tests/fute/pieCharts/main.cpp
new file mode 100644
index 0000000..ed7ce6e
--- /dev/null
+++ b/tests/fute/pieCharts/main.cpp
@@ -0,0 +1,12 @@
+#include <QtWidgets>
+
+#include "pieCharts_test.h"
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ PieChartsTest mainWin;
+ mainWin.show();
+ return app.exec();
+}
+
diff --git a/tests/fute/pieCharts/pieCharts.pro b/tests/fute/pieCharts/pieCharts.pro
new file mode 100644
index 0000000..960dd59
--- /dev/null
+++ b/tests/fute/pieCharts/pieCharts.pro
@@ -0,0 +1,29 @@
+TEMPLATE = app
+QT += widgets
+TARGET = piecharts_test
+DEPENDPATH += .
+INCLUDEPATH += .
+QMAKE_CXXFLAGS += -std=gnu++11
+DESTDIR = ./
+
+win32: {
+ CONFIG += console
+ }
+
+HEADERS = \
+ pieCharts_test.h \
+ ../../../src/charts/PieChart.h \
+ ../../../src/charts/DataPoint.h \
+ ../../../src/charts/PieChartScene.h \
+ ../../../src/charts/ChartView.h \
+ ../../../src/charts/PieRound.h \
+ ../../../src/charts/PieLegend.h
+
+SOURCES = \
+ main.cpp \
+ pieCharts_test.cpp \
+ ../../../src/charts/PieChart.cpp \
+ ../../../src/charts/PieChartScene.cpp \
+ ../../../src/charts/ChartView.cpp \
+ ../../../src/charts/PieRound.cpp \
+ ../../../src/charts/PieLegend.cpp
diff --git a/tests/fute/pieCharts/pieCharts_test.cpp b/tests/fute/pieCharts/pieCharts_test.cpp
new file mode 100644
index 0000000..d88981a
--- /dev/null
+++ b/tests/fute/pieCharts/pieCharts_test.cpp
@@ -0,0 +1,52 @@
+#include "pieCharts_test.h"
+#include "../../../src/charts/PieChart.h"
+
+#include <cstdlib>
+#include <time.h>
+
+const QStringList PieChartsTest::Labels =
+ {"Studied", "Scheduled for today", "New"};
+
+PieChartsTest::PieChartsTest()
+{
+ srand(time(NULL));
+ createUi();
+ changeDataSet();
+}
+
+void PieChartsTest::changeDataSet()
+{
+ yValuesStr = "Values: ";
+ dataSet.clear();
+ for (int i = 0; i < 3; i++)
+ addDataPoint(i);
+ chart->setDataSet(dataSet);
+ valuesLabel->setText(yValuesStr);
+}
+
+void PieChartsTest::addDataPoint(int index)
+{
+ int yValue = rand() % 200;
+ dataSet << DataPoint(Labels[index], yValue, "");
+ yValuesStr += QString::number(yValue) + ", ";
+}
+
+void PieChartsTest::createUi()
+{
+ QPushButton* newBtn = new QPushButton(tr("New chart"));
+ connect(newBtn, SIGNAL(clicked()), SLOT(changeDataSet()));
+
+ valuesLabel = new QLabel;
+ chart = new PieChart;
+ chart->setColors({"#39c900", "#ece900", "#ff0000"});
+
+ QHBoxLayout* controlLt = new QHBoxLayout;
+ controlLt->addWidget(valuesLabel);
+ controlLt->addWidget(newBtn);
+
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addLayout(controlLt);
+ mainLt->addWidget(chart);
+ setLayout(mainLt);
+ resize(800, 500);
+}
diff --git a/tests/fute/pieCharts/pieCharts_test.h b/tests/fute/pieCharts/pieCharts_test.h
new file mode 100644
index 0000000..835ff72
--- /dev/null
+++ b/tests/fute/pieCharts/pieCharts_test.h
@@ -0,0 +1,34 @@
+#ifndef PIE_CHARTS_TEST_H
+#define PIE_CHARTS_TEST_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+#include "../../../src/charts/DataPoint.h"
+
+class PieChart;
+
+class PieChartsTest: public QWidget
+{
+ Q_OBJECT
+public:
+ static const QStringList Labels;
+
+public:
+ PieChartsTest();
+
+private:
+ void createUi();
+ void addDataPoint(int index);
+
+private slots:
+ void changeDataSet();
+
+private:
+ QString yValuesStr;
+ PieChart* chart;
+ QList<DataPoint> dataSet;
+ QLabel* valuesLabel;
+};
+
+#endif
diff --git a/tests/fute/timeCharts/main.cpp b/tests/fute/timeCharts/main.cpp
new file mode 100644
index 0000000..3abb753
--- /dev/null
+++ b/tests/fute/timeCharts/main.cpp
@@ -0,0 +1,12 @@
+#include <QtWidgets>
+
+#include "timeCharts_test.h"
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ TimeChartsTest mainWin;
+ mainWin.show();
+ return app.exec();
+}
+
diff --git a/tests/fute/timeCharts/timeCharts.pro b/tests/fute/timeCharts/timeCharts.pro
new file mode 100644
index 0000000..a570829
--- /dev/null
+++ b/tests/fute/timeCharts/timeCharts.pro
@@ -0,0 +1,35 @@
+TEMPLATE = app
+QT += widgets
+TARGET = timeCharts_test
+DEPENDPATH += .
+INCLUDEPATH += .
+QMAKE_CXXFLAGS += -std=gnu++11
+DESTDIR = ./
+
+win32: {
+ CONFIG += console
+ }
+
+HEADERS = \
+ timeCharts_test.h \
+ ../../../src/charts/Chart.h \
+ ../../../src/charts/TimeChart.h \
+ ../../../src/charts/DataPoint.h \
+ ../../../src/charts/ChartScene.h \
+ ../../../src/charts/ChartView.h \
+ ../../../src/charts/ChartAxes.h \
+ ../../../src/charts/ChartDataLine.h \
+ ../../../src/charts/ChartMarker.h \
+ ../../../src/charts/ChartToolTip.h
+
+SOURCES = \
+ main.cpp \
+ timeCharts_test.cpp \
+ ../../../src/charts/Chart.cpp \
+ ../../../src/charts/TimeChart.cpp \
+ ../../../src/charts/ChartScene.cpp \
+ ../../../src/charts/ChartView.cpp \
+ ../../../src/charts/ChartAxes.cpp \
+ ../../../src/charts/ChartDataLine.cpp \
+ ../../../src/charts/ChartMarker.cpp \
+ ../../../src/charts/ChartToolTip.cpp
diff --git a/tests/fute/timeCharts/timeCharts_test.cpp b/tests/fute/timeCharts/timeCharts_test.cpp
new file mode 100644
index 0000000..53f0921
--- /dev/null
+++ b/tests/fute/timeCharts/timeCharts_test.cpp
@@ -0,0 +1,58 @@
+#include "../../../src/charts/TimeChart.h"
+
+#include <cstdlib>
+#include <time.h>
+#include "timeCharts_test.h"
+
+TimeChartsTest::TimeChartsTest()
+{
+ srand(time(NULL));
+ createUi();
+ changeDataSet();
+}
+
+void TimeChartsTest::changeDataSet()
+{
+ int daysNum = periodBox->value();
+ yValuesStr = "Values: ";
+ dates.clear();
+ for (int i = 0; i < daysNum; i++)
+ addDataPoint(i);
+ chart->setDates(dates, daysNum, 1);
+ valuesLabel->setText(yValuesStr);
+}
+
+void TimeChartsTest::addDataPoint(int index)
+{
+ QDateTime date = QDateTime::currentDateTime().addDays(index);
+ int dayCardsNum = rand() % 50;
+ for(int i = 0; i < dayCardsNum; i++)
+ dates << date;
+ yValuesStr += QString::number(dayCardsNum) + ", ";
+}
+
+void TimeChartsTest::createUi()
+{
+ QPushButton* newBtn = new QPushButton(tr("New chart"));
+ connect(newBtn, SIGNAL(clicked()), SLOT(changeDataSet()));
+
+ valuesLabel = new QLabel;
+ valuesLabel->setMaximumWidth(750);
+
+ periodBox = new QSpinBox;
+ periodBox->setRange(7, 5000);
+
+ chart = new TimeChart;
+ chart->setLabels(tr("Date"), tr("Cards"));
+
+ QHBoxLayout* controlLt = new QHBoxLayout;
+ controlLt->addWidget(periodBox);
+ controlLt->addWidget(newBtn);
+
+ QVBoxLayout* mainLt = new QVBoxLayout;
+ mainLt->addLayout(controlLt);
+ mainLt->addWidget(valuesLabel);
+ mainLt->addWidget(chart);
+ setLayout(mainLt);
+ resize(800, 500);
+}
diff --git a/tests/fute/timeCharts/timeCharts_test.h b/tests/fute/timeCharts/timeCharts_test.h
new file mode 100644
index 0000000..49d6346
--- /dev/null
+++ b/tests/fute/timeCharts/timeCharts_test.h
@@ -0,0 +1,30 @@
+#ifndef CHARTS_TEST_H
+#define CHARTS_TEST_H
+
+#include <QtCore>
+#include <QtWidgets>
+
+class TimeChart;
+
+class TimeChartsTest: public QWidget
+{
+ Q_OBJECT
+public:
+ TimeChartsTest();
+
+private:
+ void createUi();
+ void addDataPoint(int index);
+
+private slots:
+ void changeDataSet();
+
+private:
+ QString yValuesStr;
+ TimeChart* chart;
+ QList<QDateTime> dates;
+ QLabel* valuesLabel;
+ QSpinBox* periodBox;
+};
+
+#endif
diff --git a/tests/mocks/CardPack_mock.cpp b/tests/mocks/CardPack_mock.cpp
new file mode 100644
index 0000000..159e0de
--- /dev/null
+++ b/tests/mocks/CardPack_mock.cpp
@@ -0,0 +1,31 @@
+#include "CardPack_mock.h"
+
+void CardPackMock::addStudyRecord(const QString cardId, const StudyRecord& studyRecord)
+{
+ studyRecords.insert(cardId, studyRecord);
+}
+
+QList<StudyRecord> CardPackMock::getStudyRecords(QString cardId) const
+{
+ return studyRecords.values(cardId);
+}
+
+StudyRecord CardPackMock::getStudyRecord(QString cardId) const
+{
+ return studyRecords.values(cardId).first();
+}
+
+QList<DicRecord*> CardPackMock::getRecords() const
+{
+ return QList<DicRecord*>();
+}
+
+const Field* CardPackMock::getQuestionField() const
+{
+ return NULL;
+}
+
+QList<const Field*> CardPackMock::getAnswerFields() const
+{
+ return QList<const Field*>();
+}
diff --git a/tests/mocks/CardPack_mock.h b/tests/mocks/CardPack_mock.h
new file mode 100644
index 0000000..50e6c13
--- /dev/null
+++ b/tests/mocks/CardPack_mock.h
@@ -0,0 +1,23 @@
+#ifndef CARDPACK_MOCK_H
+#define CARDPACK_MOCK_H
+
+#include <QtCore>
+
+#include "../../src/dictionary/ICardPack.h"
+
+class CardPackMock: public ICardPack
+{
+public:
+ void addStudyRecord(const QString cardId, const StudyRecord& studyRecord);
+ QList<StudyRecord> getStudyRecords(QString cardId) const;
+ StudyRecord getStudyRecord(QString cardId) const;
+
+ QList<DicRecord*> getRecords() const;
+ const Field* getQuestionField() const;
+ QList<const Field*> getAnswerFields() const;
+
+private:
+ QMultiHash< QString, StudyRecord > studyRecords;
+};
+
+#endif
diff --git a/tests/mocks/Dictionary_mock.cpp b/tests/mocks/Dictionary_mock.cpp
new file mode 100644
index 0000000..905a2f2
--- /dev/null
+++ b/tests/mocks/Dictionary_mock.cpp
@@ -0,0 +1,6 @@
+#include "Dictionary_mock.h"
+
+void MockDictionary::addCardPack(CardPack* aCardPack)
+{
+ m_cardPacks << aCardPack;
+}
diff --git a/tests/mocks/Dictionary_mock.h b/tests/mocks/Dictionary_mock.h
new file mode 100644
index 0000000..0b29fbb
--- /dev/null
+++ b/tests/mocks/Dictionary_mock.h
@@ -0,0 +1,32 @@
+#ifndef DICTIONARY_MOCK_H
+#define DICTIONARY_MOCK_H
+
+#include <QtCore>
+
+#include "../../src/dictionary/IDictionary.h"
+#include "../../src/dictionary/TreeItem.h"
+
+class MockDictionary: public TreeItem, public IDictionary
+{
+Q_OBJECT
+public:
+ const TreeItem* parent() const { return NULL; }
+ const TreeItem* child(int) const { return NULL; }
+ int childCount() const { return 0; }
+ int columnCount() const { return 0; }
+ QVariant data(int) const { return QVariant(); }
+ int row() const { return 0; }
+ int topParentRow() const { return 0; }
+
+ const Field* field(int) const { return NULL; }
+ const Field* field(const QString) const { return NULL; }
+ int indexOfCardPack(CardPack*) const { return 0; }
+
+ void addCardPack(CardPack* aCardPack);
+
+signals:
+ void entryChanged( int aEntryIx, int aFieldIx );
+ void entriesRemoved( int aIndex, int aNum );
+};
+
+#endif
diff --git a/tests/mocks/RandomGenerator_mock.h b/tests/mocks/RandomGenerator_mock.h
new file mode 100644
index 0000000..0d3ef97
--- /dev/null
+++ b/tests/mocks/RandomGenerator_mock.h
@@ -0,0 +1,33 @@
+#ifndef RANDOM_GENERATOR_MOCK_H
+#define RANDOM_GENERATOR_MOCK_H
+
+#include "../../src/utils/IRandomGenerator.h"
+
+class MockRandomGenerator: public IRandomGenerator
+{
+public:
+ MockRandomGenerator():
+ dRandom(0), rand(0) {}
+ double getInRange_11() const { return dRandom; }
+ double getInRange_01() const { return dRandom; }
+ int getRand() const { return rand; }
+ int getRand(int maxNum) const
+ {
+ int r = rand;
+ if(r >= maxNum)
+ r = maxNum -1;
+ return r;
+ }
+ QByteArray getArray() const { return array; }
+
+ void setDouble(double dRandom) { this->dRandom = dRandom; }
+ void setRand(int rand) { this->rand = rand; }
+ void setArray(const QByteArray& array) { this->array = array; }
+
+private:
+ double dRandom;
+ int rand;
+ QByteArray array;
+};
+
+#endif
diff --git a/tests/mocks/TimeProvider_mock.cpp b/tests/mocks/TimeProvider_mock.cpp
new file mode 100644
index 0000000..22cfb43
--- /dev/null
+++ b/tests/mocks/TimeProvider_mock.cpp
@@ -0,0 +1,9 @@
+#include "../../src/utils/TimeProvider.h"
+
+// TODO: Make interface with virtual function. Use Time provider as singleton.
+
+QDateTime TimeProvider::get()
+{
+ static QDateTime time = QDateTime::currentDateTime();
+ return time;
+}
diff --git a/tests/unit/Card/Card_GenerateAnswers_test.cpp b/tests/unit/Card/Card_GenerateAnswers_test.cpp
new file mode 100644
index 0000000..f381daf
--- /dev/null
+++ b/tests/unit/Card/Card_GenerateAnswers_test.cpp
@@ -0,0 +1,66 @@
+#include "Card_GenerateAnswers_test.h"
+
+#include <initializer_list>
+
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/IDictionary.h"
+#include "../../../src/dictionary/Field.h"
+#include "../../../src/dictionary/DicRecord.h"
+#include "../../../src/dictionary/Card.h"
+
+INSTANTIATE_TEST_CASE_P(, GenerateAnswersTest,
+ testing::ValuesIn(RecordsParam::createParams()) );
+
+void GenerateAnswersTest::TearDown()
+{
+ for(Field* field: fields)
+ delete field;
+}
+
+TEST_P(GenerateAnswersTest, generateAnswers)
+ {
+ auto param = GetParam();
+ dict.addRecords(param.records);
+ for(int i: param.packFields)
+ addFieldToPack(i);
+
+ unsigned i = 0;
+ for(vector<string> expectedCardFields: param.answers)
+ {
+ SCOPED_TRACE(i);
+ Card card(&pack, expectedCardFields[0].c_str());
+ for(unsigned j = 1; j < expectedCardFields.size(); j++)
+ {
+ SCOPED_TRACE(j);
+ ASSERT_EQ_QSTR(QString(expectedCardFields[j].c_str()),
+ card.getAnswers()[j - 1]);
+ }
+ i++;
+ }
+}
+
+TEST_F(GenerateAnswersTest, dropAnswers)
+ {
+ const vector<string> fieldValues {"table", "стол"};
+
+ DicRecord* record = new DicRecord;
+ dict.addRecord(record);
+ for(unsigned i = 0; i < fieldValues.size(); i++)
+ {
+ addFieldToPack(i);
+ record->setField(RecordsParam::fieldNames[i].c_str(), fieldValues[i].c_str());
+ }
+ Card card(&pack, fieldValues[0].c_str());
+
+ ASSERT_EQ(QString(fieldValues[1].c_str()), card.getAnswers().first());
+
+ record->setField("Russian", "кровать");
+ ASSERT_EQ(QString("кровать"), card.getAnswers().first());
+}
+
+void GenerateAnswersTest::addFieldToPack(unsigned fieldId)
+{
+ Field* field = new Field(RecordsParam::fieldNames[fieldId].c_str(), "Normal");
+ fields.push_back(field);
+ pack.addField(field);
+}
diff --git a/tests/unit/Card/Card_GenerateAnswers_test.h b/tests/unit/Card/Card_GenerateAnswers_test.h
new file mode 100644
index 0000000..1b69cba
--- /dev/null
+++ b/tests/unit/Card/Card_GenerateAnswers_test.h
@@ -0,0 +1,34 @@
+#include <vector>
+#include <string>
+#include <gtest/gtest.h>
+
+#include "../../../src/dictionary/CardPack.h"
+#include "../../mocks/Dictionary_mock.h"
+#include "../../common/RecordsParam.h"
+
+class Field;
+
+using std::vector;
+using std::string;
+
+class GenerateAnswersTest: public testing::TestWithParam<RecordsParam>
+{
+public:
+ GenerateAnswersTest():
+ pack(&dict) {}
+
+protected:
+ void TearDown();
+ void addFieldToPack(unsigned fieldId);
+
+public:
+ static const vector<string> fieldNames;
+
+protected:
+ vector<Field*> fields;
+
+protected:
+ MockDictionary dict;
+ CardPack pack;
+};
+
diff --git a/tests/unit/Card/Card_test.cpp b/tests/unit/Card/Card_test.cpp
new file mode 100644
index 0000000..bfae243
--- /dev/null
+++ b/tests/unit/Card/Card_test.cpp
@@ -0,0 +1,36 @@
+#include <gtest/gtest.h>
+#include <QtCore>
+
+#include "Card_test.h"
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../../src/dictionary/ICardPack.h"
+#include "../../mocks/CardPack_mock.h"
+
+void CardTest::SetUp()
+ {
+ defaultPack = new CardPackMock;
+ }
+
+void CardTest::TearDown()
+ {
+ delete defaultPack;
+ }
+
+TEST_F(CardTest, Create)
+ {
+ Card card(defaultPack);
+
+ ASSERT_EQ(defaultPack, card.getCardPack());
+ ASSERT_TRUE(card.getQuestion().isEmpty());
+ ASSERT_TRUE(card.getAnswers().isEmpty());
+ }
+
+TEST_F(CardTest, Create_NullPack)
+ {
+ Card card(NULL);
+
+ ASSERT_EQ(NULL, card.getCardPack());
+ ASSERT_TRUE(card.getQuestion().isEmpty());
+ ASSERT_TRUE(card.getAnswers().isEmpty());
+ }
diff --git a/tests/unit/Card/Card_test.h b/tests/unit/Card/Card_test.h
new file mode 100644
index 0000000..e9a0891
--- /dev/null
+++ b/tests/unit/Card/Card_test.h
@@ -0,0 +1,18 @@
+#ifndef CARD_TEST_H
+#define CARD_TEST_H
+
+#include <gtest/gtest.h>
+
+class ICardPack;
+
+class CardTest: public testing::Test
+{
+public:
+ void SetUp();
+ void TearDown();
+
+protected:
+ ICardPack* defaultPack;
+};
+
+#endif
diff --git a/tests/unit/Card/Card_test_QuestionAnswer.cpp b/tests/unit/Card/Card_test_QuestionAnswer.cpp
new file mode 100644
index 0000000..6100f75
--- /dev/null
+++ b/tests/unit/Card/Card_test_QuestionAnswer.cpp
@@ -0,0 +1,30 @@
+#include <gtest/gtest.h>
+
+#include "Card_test.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../mocks/CardPack_mock.h"
+
+using testing::Combine;
+using testing::Values;
+using testing::WithParamInterface;
+
+class QuestionCardTest: public CardTest,
+ public WithParamInterface<const char*>
+{};
+
+INSTANTIATE_TEST_CASE_P(, QuestionCardTest,
+ Values("", "Question"));
+
+TEST_P(QuestionCardTest, getName)
+ {
+ QString question = GetParam();
+ Card card(defaultPack, question);
+ ASSERT_EQ(question, card.getName());
+ }
+
+TEST_P(QuestionCardTest, getQuestion)
+ {
+ QString question = GetParam();
+ Card card(defaultPack, question);
+ ASSERT_EQ(question, card.getQuestion());
+ }
diff --git a/tests/unit/Card/card.pri b/tests/unit/Card/card.pri
new file mode 100644
index 0000000..2eae07d
--- /dev/null
+++ b/tests/unit/Card/card.pri
@@ -0,0 +1,10 @@
+HEADERS += \
+ $$PWD/Card_test.h \
+ $$PWD/Card_GenerateAnswers_test.h \
+ $$TESTS/mocks/CardPack_mock.h
+
+SOURCES += \
+ $$PWD/Card_test.cpp \
+ $$PWD/Card_test_QuestionAnswer.cpp \
+ $$PWD/Card_GenerateAnswers_test.cpp \
+ $$TESTS/mocks/CardPack_mock.cpp
diff --git a/tests/unit/CardPack/CardPack_GenerateCards_test.cpp b/tests/unit/CardPack/CardPack_GenerateCards_test.cpp
new file mode 100644
index 0000000..c7e7fcb
--- /dev/null
+++ b/tests/unit/CardPack/CardPack_GenerateCards_test.cpp
@@ -0,0 +1,43 @@
+#include "CardPack_GenerateCards_test.h"
+
+#include <initializer_list>
+
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/IDictionary.h"
+#include "../../../src/dictionary/Field.h"
+#include "../../../src/dictionary/DicRecord.h"
+
+vector<Field*> GenerateCardsTest::fields;
+
+INSTANTIATE_TEST_CASE_P(, GenerateCardsTest,
+ testing::ValuesIn(RecordsParam::createParams()) );
+
+vector<Field*> GenerateCardsTest::getFields()
+{
+ static vector<string> names {"English", "Russian"};
+ if(fields.empty())
+ for(string name: names)
+ fields.push_back(new Field(name.c_str(), "Normal"));
+ return fields;
+}
+
+void GenerateCardsTest::TearDownTestCase()
+{
+ for(Field* field: fields)
+ delete field;
+}
+
+TEST_P(GenerateCardsTest, generateQuestions)
+ {
+ auto param = GetParam();
+ dict.addRecords(param.records);
+ for(int fieldId: param.packFields)
+ pack.addField(getFields()[ fieldId ]);
+ pack.generateQuestions();
+
+ QStringList questions;
+ for(string question: param.questions)
+ questions << question.c_str();
+
+ ASSERT_EQ(questions, pack.getCardQuestions());
+}
diff --git a/tests/unit/CardPack/CardPack_GenerateCards_test.h b/tests/unit/CardPack/CardPack_GenerateCards_test.h
new file mode 100644
index 0000000..299ee66
--- /dev/null
+++ b/tests/unit/CardPack/CardPack_GenerateCards_test.h
@@ -0,0 +1,26 @@
+#include <iostream>
+#include <vector>
+#include <string>
+#include <gtest/gtest.h>
+#include <QtCore>
+
+#include "CardPack_test.h"
+#include "../../common/RecordsParam.h"
+
+class DicRecord;
+
+using std::vector;
+using std::string;
+using std::ostream;
+
+class GenerateCardsTest: public CardPackTest,
+ public testing::WithParamInterface<RecordsParam>
+{
+public:
+ static void TearDownTestCase();
+ static vector<Field*> getFields();
+
+private:
+ static vector<Field*> fields;
+
+};
diff --git a/tests/unit/CardPack/CardPack_test.cpp b/tests/unit/CardPack/CardPack_test.cpp
new file mode 100644
index 0000000..47b2387
--- /dev/null
+++ b/tests/unit/CardPack/CardPack_test.cpp
@@ -0,0 +1,80 @@
+#include "CardPack_test.h"
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/IDictionary.h"
+#include "../../../src/dictionary/Field.h"
+#include "../../../src/dictionary/DicRecord.h"
+
+ostream& operator<<(ostream& os, const Field* field)
+{
+ return os << field->name().toStdString();
+}
+
+CardPackTest::CardPackTest():
+ pack(&dict),
+ qstField("Question", "Normal"),
+ ansField1("Answer1", "Normal"),
+ ansField2("Answer2", "Normal")
+{}
+
+TEST_F(CardPackTest, empty_QuestionAnswers)
+ {
+ ASSERT_EQ(NULL, pack.getQuestionField());
+ ASSERT_EQ(QList<const Field*>(), pack.getAnswerFields());
+ }
+
+TEST_F(CardPackTest, empty_Id)
+ {
+ ASSERT_EQ("(empty pack)", pack.id());
+ }
+
+TEST_F(CardPackTest, empty_cardQuestions)
+ {
+ ASSERT_EQ(QStringList(), pack.getCardQuestions());
+ }
+
+TEST_F(CardPackTest, setQstField)
+ {
+ pack.setQstField(&qstField);
+ ASSERT_EQ(&qstField, pack.getQuestionField());
+ }
+
+TEST_F(CardPackTest, setAnsField_1)
+ {
+ ansFields << &ansField1;
+
+ pack.setAnsFields(ansFields);
+ ASSERT_EQ(ansFields, pack.getAnswerFields());
+ }
+
+TEST_F(CardPackTest, setAnsFields_2)
+ {
+ ansFields << &ansField1 << &ansField2;
+
+ pack.setAnsFields(ansFields);
+ ASSERT_EQ(ansFields, pack.getAnswerFields());
+ }
+
+TEST_F(CardPackTest, addFields)
+ {
+ QList<const Field*> allFields;
+ allFields << &qstField << &ansField1 << &ansField2;
+ ansFields << &ansField1 << &ansField2;
+
+ pack.addField(&qstField);
+ pack.addField(&ansField1);
+ pack.addField(&ansField2);
+
+ ASSERT_EQ(allFields, pack.getFields());
+ ASSERT_EQ(&qstField, pack.getQuestionField());
+ ASSERT_EQ(ansFields, pack.getAnswerFields());
+ }
+
+TEST_F(CardPackTest, id)
+ {
+ ansFields << &ansField1 << &ansField2;
+
+ pack.setQstField(&qstField);
+ pack.setAnsFields(ansFields);
+
+ ASSERT_EQ("Question - Answer1, Answer2", pack.id());
+ }
diff --git a/tests/unit/CardPack/CardPack_test.h b/tests/unit/CardPack/CardPack_test.h
new file mode 100644
index 0000000..e25c75d
--- /dev/null
+++ b/tests/unit/CardPack/CardPack_test.h
@@ -0,0 +1,31 @@
+#ifndef CARDPACK_TEST_H
+#define CARDPACK_TEST_H
+
+#include <gtest/gtest.h>
+#include <iostream>
+#include <QtCore>
+
+#include "../../../src/dictionary/CardPack.h"
+#include "../../mocks/Dictionary_mock.h"
+
+using std::ostream;
+
+class Field;
+
+class CardPackTest: public testing::Test
+{
+public:
+ CardPackTest();
+
+protected:
+ MockDictionary dict;
+ CardPack pack;
+ Field qstField;
+ Field ansField1;
+ Field ansField2;
+ QList<const Field*> ansFields;
+};
+
+ostream& operator<<(ostream& os, const Field* field);
+
+#endif
diff --git a/tests/unit/CardPack/cPack.pri b/tests/unit/CardPack/cPack.pri
new file mode 100644
index 0000000..e3befe3
--- /dev/null
+++ b/tests/unit/CardPack/cPack.pri
@@ -0,0 +1,7 @@
+HEADERS += \
+ $$PWD/CardPack_test.h \
+ $$PWD/CardPack_GenerateCards_test.h
+
+SOURCES += \
+ $$PWD/CardPack_test.cpp \
+ $$PWD/CardPack_GenerateCards_test.cpp
diff --git a/tests/unit/CardSideView/CardSideView_test.cpp b/tests/unit/CardSideView/CardSideView_test.cpp
new file mode 100644
index 0000000..9b08dc0
--- /dev/null
+++ b/tests/unit/CardSideView/CardSideView_test.cpp
@@ -0,0 +1,49 @@
+#include <gtest/gtest.h>
+#include <QtCore>
+#include <QtWidgets>
+
+#include "CardSideView_test.h"
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../../src/study/CardSideView.h"
+
+void CardSideViewTest::SetUp()
+ {
+ cardPack.addField(new Field("English", "Normal"));
+ cardPack.addField(new Field("Example", "Example"));
+ cardPack.addField(new Field("Russian", "Normal"));
+ }
+
+TEST_F(CardSideViewTest, getFormattedQuestion)
+ {
+ CardSideView view;
+ view.setPack(&cardPack);
+ view.setQstAnsr("First", QStringList());
+ ASSERT_EQ("<span style=\"font-family:'Times New Roman'; "\
+ "font-size:18pt; font-weight:bold\">First</span>",
+ view.getFormattedText());
+ }
+
+TEST_F(CardSideViewTest, getFormattedAnswer)
+ {
+ CardSideView view(CardSideView::AnsMode);
+ view.setPack(&cardPack);
+ QStringList answers = QStringList() << "First example" << "Pervyj";
+ view.setQstAnsr("First", answers);
+ ASSERT_EQ("<span style=\"font-family:'Times New Roman'; "\
+ "font-size:14pt\"><span style=\"; color:#0000ff\">First</span> example</span><br/><br/>"\
+ "<span style=\"font-family:'Times New Roman'; font-size:18pt; "\
+ "font-weight:bold\">Pervyj</span>",
+ view.getFormattedText());
+ }
+
+TEST_F(CardSideViewTest, getFormattedAnswer_1missing)
+ {
+ CardSideView view(CardSideView::AnsMode);
+ view.setPack(&cardPack);
+ QStringList answers = QStringList() << "" << "Pervyj";
+ view.setQstAnsr("First", answers);
+ ASSERT_EQ("<br/><br/><span style=\"font-family:'Times New Roman'; font-size:18pt; "\
+ "font-weight:bold\">Pervyj</span>",
+ view.getFormattedText());
+ }
diff --git a/tests/unit/CardSideView/CardSideView_test.h b/tests/unit/CardSideView/CardSideView_test.h
new file mode 100644
index 0000000..491ebe4
--- /dev/null
+++ b/tests/unit/CardSideView/CardSideView_test.h
@@ -0,0 +1,21 @@
+#ifndef CARDSIDEVIEW_TEST_H
+#define CARDSIDEVIEW_TEST_H
+
+#include <gtest/gtest.h>
+
+#include "../../mocks/Dictionary_mock.h"
+#include "../../../src/dictionary/CardPack.h"
+
+class CardSideViewTest: public testing::Test
+{
+public:
+ CardSideViewTest():
+ cardPack(&dict) {}
+ void SetUp();
+
+protected:
+ MockDictionary dict;
+ CardPack cardPack;
+};
+
+#endif
diff --git a/tests/unit/CardSideView/csView.pri b/tests/unit/CardSideView/csView.pri
new file mode 100644
index 0000000..6eeae5f
--- /dev/null
+++ b/tests/unit/CardSideView/csView.pri
@@ -0,0 +1,8 @@
+HEADERS += \
+ $$PWD/CardSideView_test.h \
+ $$SRC/study/CardSideView.h
+
+SOURCES += \
+ $$PWD/CardSideView_test.cpp \
+ $$SRC/study/CardSideView.cpp
+ \ No newline at end of file
diff --git a/tests/unit/RandomGenerator/RandomGenerator_test.cpp b/tests/unit/RandomGenerator/RandomGenerator_test.cpp
new file mode 100644
index 0000000..d334c9b
--- /dev/null
+++ b/tests/unit/RandomGenerator/RandomGenerator_test.cpp
@@ -0,0 +1,79 @@
+#include <gtest/gtest.h>
+#include <cmath>
+#include "../../../src/utils/RandomGenerator.h"
+
+class RandomGeneratorTest: public testing::Test
+{
+protected:
+ void generateRandoms();
+ void checkStats();
+
+private:
+ void checkMinMeanMax(double min, double mean, double max);
+ void minMeanMax(double& min, double& mean, double& max);
+ double stddev(double mean);
+
+protected:
+ static const int Num = 10000;
+
+protected:
+ RandomGenerator random;
+ double x[Num];
+};
+
+TEST_F(RandomGeneratorTest, Stats)
+{
+ generateRandoms();
+ checkStats();
+}
+
+TEST_F(RandomGeneratorTest, getArray)
+{
+ QByteArray ba = random.getArray();
+}
+
+void RandomGeneratorTest::generateRandoms()
+{
+ for(int i = 0; i < Num; i++)
+ x[i] = random.getInRange_11();
+}
+
+void RandomGeneratorTest::checkStats()
+{
+ double min;
+ double mean;
+ double max;
+ minMeanMax(min, mean, max);
+
+ checkMinMeanMax(min, mean, max);
+ ASSERT_GT(stddev(mean), 0.55);
+}
+
+void RandomGeneratorTest::checkMinMeanMax(double min, double mean, double max)
+{
+ ASSERT_NEAR(-1, min, 0.01);
+ ASSERT_NEAR(0, mean, 0.2);
+ ASSERT_NEAR(1, max, 0.01);
+}
+
+void RandomGeneratorTest::minMeanMax(double& min, double& mean, double& max)
+{
+ double sum = 0;
+ min = 1000;
+ max = -1000;
+ for(int i = 0; i < Num; i++)
+ {
+ min = fmin(x[i], min);
+ max = fmax(x[i], max);
+ sum += x[i];
+ }
+ mean = sum / Num;
+}
+
+double RandomGeneratorTest::stddev(double mean)
+{
+ double varSum = 0;
+ for(int i = 0; i < Num; i++)
+ varSum += pow((mean - x[i]), 2);
+ return sqrt(varSum / Num);
+}
diff --git a/tests/unit/RandomGenerator/rndGen.pri b/tests/unit/RandomGenerator/rndGen.pri
new file mode 100644
index 0000000..dbc9300
--- /dev/null
+++ b/tests/unit/RandomGenerator/rndGen.pri
@@ -0,0 +1,6 @@
+HEADERS += \
+ $$SRC/utils/RandomGenerator.h
+
+SOURCES += \
+ $$PWD/RandomGenerator_test.cpp \
+ $$SRC/utils/RandomGenerator.cpp
diff --git a/tests/unit/Settings/FieldStyleFactory_test.cpp b/tests/unit/Settings/FieldStyleFactory_test.cpp
new file mode 100644
index 0000000..be53e7c
--- /dev/null
+++ b/tests/unit/Settings/FieldStyleFactory_test.cpp
@@ -0,0 +1,88 @@
+#include "FieldStyleFactory_test.h"
+#include "../../common/printQtTypes.h"
+#include "TestSettings.h"
+
+void FieldStyleFactoryTest::SetUp()
+{
+ TestSettings::init();
+}
+
+FieldStyleFactory FieldStyleFactoryTest::getDefaults()
+{
+ FieldStyleFactory factory;
+ factory.cardBgColor.setNamedColor("#ffffff");
+ factory.setStyle(FieldStyleFactory::DefaultStyle,
+ {"Times New Roman", 18, true, false, "black" , "", ""});
+ factory.setStyle("Example",
+ {"Times New Roman", 14, false, false, "black", "", "", true, "blue"});
+ factory.setStyle("Transcription",
+ {"Arial", 18, false, false, "black", "/", "/"});
+ factory.setStyle("Big", {"Arial", 26, true, false, "black"});
+ factory.setStyle("Color1", {"Times New Roman", 18, true, false, "red"});
+ factory.setStyle("Color2", {"Times New Roman", 18, true, false, "blue"});
+ return factory;
+}
+
+FieldStyleFactory FieldStyleFactoryTest::getUserSettings()
+{
+ FieldStyleFactory factory;
+ factory.cardBgColor.setNamedColor("#ffff00");
+ factory.setStyle(FieldStyleFactory::DefaultStyle,
+ {"Georgia", 19, false, true, "#550000", "", ""});
+ factory.setStyle("Example",
+ {"Times New Roman", 15, true, false, "black", "", ""});
+ factory.setStyle("Transcription",
+ {"Verdana", 18, true, false, "black", "[", "]"});
+ factory.setStyle("Big",
+ {"Arial", 27, false, false, "black", "", "", true, "blue"});
+ factory.setStyle("Color1", {"Times New Roman", 18, true, false, "green"});
+ factory.setStyle("Color2", {"Times New Roman", 18, true, false, "#00FFFF"});
+ return factory;
+}
+
+TEST_F(FieldStyleFactoryTest, DefaultValues)
+{
+ TestFieldStyleFactory factory;
+ SCOPED_TRACE("Default values");
+ factory.check(getDefaults());
+}
+
+TEST_F(FieldStyleFactoryTest, UserValues)
+{
+ FieldStyleFactory::inst()->load();
+ SCOPED_TRACE("User values");
+ static_cast<TestFieldStyleFactory*>(FieldStyleFactory::inst())->
+ check(getUserSettings());
+}
+
+void TestFieldStyleFactory::check(const FieldStyleFactory& expFactory)
+{
+ ASSERT_EQ(expFactory.cardBgColor.name(), cardBgColor.name());
+ checkStyles(expFactory);
+}
+
+void TestFieldStyleFactory::checkStyles(const FieldStyleFactory& expFactory)
+{
+ ASSERT_EQ(expFactory.getStyleNames(), getStyleNames());
+ foreach(QString styleName, getStyleNames())
+ {
+ SCOPED_TRACE(styleName.toStdString());
+ checkStyle(expFactory.getStyle(styleName), styleName);
+ }
+}
+
+void TestFieldStyleFactory::checkStyle(const FieldStyle& expStyle,
+ const QString& actualStyleName)
+{
+ FieldStyle actualStyle = getStyle(actualStyleName);
+ ASSERT_EQ(expStyle.font.family(), actualStyle.font.family());
+ ASSERT_EQ(expStyle.font.pointSize(), actualStyle.font.pointSize());
+ ASSERT_EQ(expStyle.font.bold(), actualStyle.font.bold());
+ ASSERT_EQ(expStyle.font.italic(), actualStyle.font.italic());
+ ASSERT_EQ(expStyle.color.name(), actualStyle.color.name());
+ ASSERT_EQ(expStyle.prefix, actualStyle.prefix);
+ ASSERT_EQ(expStyle.suffix, actualStyle.suffix);
+ ASSERT_EQ(expStyle.hasKeyword, actualStyle.hasKeyword);
+ if(expStyle.hasKeyword)
+ ASSERT_EQ(expStyle.keywordColor.name(), actualStyle.keywordColor.name());
+}
diff --git a/tests/unit/Settings/FieldStyleFactory_test.h b/tests/unit/Settings/FieldStyleFactory_test.h
new file mode 100644
index 0000000..9b7d410
--- /dev/null
+++ b/tests/unit/Settings/FieldStyleFactory_test.h
@@ -0,0 +1,29 @@
+#ifndef FIELD_STYLE_FACTORY_TEST_H
+#define FIELD_STYLE_FACTORY_TEST_H
+
+#include <gtest/gtest.h>
+#include <QtCore>
+
+#include "../../../src/field-styles/FieldStyleFactory.h"
+
+class TestFieldStyleFactory: public FieldStyleFactory
+{
+public:
+ void check(const FieldStyleFactory& expFactory);
+
+private:
+ void checkStyles(const FieldStyleFactory& expFactory);
+ void checkStyle(const FieldStyle& expStyle, const QString& actualStyleName);
+};
+
+class FieldStyleFactoryTest: public testing::Test
+{
+public:
+ static FieldStyleFactory getDefaults();
+ static FieldStyleFactory getUserSettings();
+
+public:
+ void SetUp();
+};
+
+#endif
diff --git a/tests/unit/Settings/StudySettings_test.cpp b/tests/unit/Settings/StudySettings_test.cpp
new file mode 100644
index 0000000..d66b8a1
--- /dev/null
+++ b/tests/unit/Settings/StudySettings_test.cpp
@@ -0,0 +1,42 @@
+#include "StudySettings_test.h"
+#include "../../common/printQtTypes.h"
+#include "TestSettings.h"
+
+void StudySettingsTest::SetUp()
+{
+ TestSettings::init();
+}
+
+StudySettings StudySettingsTest::getDefaults()
+{
+ StudySettings defaults;
+ defaults.dayShift = 3;
+ return defaults;
+}
+
+StudySettings StudySettingsTest::getUserSettings()
+{
+
+ StudySettings user;
+ user.dayShift = 4;
+ return user;
+}
+
+TEST_F(StudySettingsTest, DefaultValues)
+{
+ StudySettings settings;
+ SCOPED_TRACE("Default values");
+ check(getDefaults(), settings);
+}
+
+TEST_F(StudySettingsTest, UserValues)
+{
+ StudySettings::inst()->load();
+ SCOPED_TRACE("User values");
+ check(getUserSettings(), *StudySettings::inst());
+}
+
+void StudySettingsTest::check(const StudySettings& expected, const StudySettings& actual)
+{
+ ASSERT_EQ(expected.dayShift, actual.dayShift);
+}
diff --git a/tests/unit/Settings/StudySettings_test.h b/tests/unit/Settings/StudySettings_test.h
new file mode 100644
index 0000000..aa45ead
--- /dev/null
+++ b/tests/unit/Settings/StudySettings_test.h
@@ -0,0 +1,22 @@
+#ifndef STUDY_SETTINGS_TEST_H
+#define STUDY_SETTINGS_TEST_H
+
+#include <gtest/gtest.h>
+#include <QtCore>
+
+#include "../../../src/study/StudySettings.h"
+
+class StudySettingsTest: public testing::Test
+{
+public:
+ static StudySettings getDefaults();
+ static StudySettings getUserSettings();
+
+protected:
+ void check(const StudySettings& expected, const StudySettings& actual);
+
+public:
+ void SetUp();
+};
+
+#endif
diff --git a/tests/unit/Settings/TestSettings.cpp b/tests/unit/Settings/TestSettings.cpp
new file mode 100644
index 0000000..1f3fbb4
--- /dev/null
+++ b/tests/unit/Settings/TestSettings.cpp
@@ -0,0 +1,16 @@
+#include <QtCore>
+#include <gtest/gtest.h>
+
+#include "TestSettings.h"
+
+void TestSettings::init()
+{
+ QSettings::setDefaultFormat(QSettings::IniFormat);
+ QCoreApplication::setOrganizationName("freshmemory");
+ QCoreApplication::setApplicationName("freshmemory");
+ QString appDir = qApp->applicationDirPath();
+ QString userSettingsDir = appDir + "/../common";
+ QString systemSettingsDir = appDir + "/../../config";
+ QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, userSettingsDir);
+ QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, systemSettingsDir);
+}
diff --git a/tests/unit/Settings/TestSettings.h b/tests/unit/Settings/TestSettings.h
new file mode 100644
index 0000000..60b8be8
--- /dev/null
+++ b/tests/unit/Settings/TestSettings.h
@@ -0,0 +1,11 @@
+#ifndef TEST_SETTINGS_H
+#define TEST_SETTINGS_H
+
+class TestSettings
+{
+public:
+ static void init();
+
+};
+
+#endif
diff --git a/tests/unit/Settings/set.pri b/tests/unit/Settings/set.pri
new file mode 100644
index 0000000..df1871f
--- /dev/null
+++ b/tests/unit/Settings/set.pri
@@ -0,0 +1,10 @@
+HEADERS += \
+ $$PWD/TestSettings.h \
+ $$PWD/StudySettings_test.h \
+ $$PWD/FieldStyleFactory_test.h
+
+SOURCES += \
+ $$PWD/TestSettings.cpp \
+ $$PWD/StudySettings_test.cpp \
+ $$PWD/FieldStyleFactory_test.cpp
+ \ No newline at end of file
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.cpp b/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.cpp
new file mode 100644
index 0000000..73ab023
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.cpp
@@ -0,0 +1,420 @@
+#include "SRModel_pickCard_test.h"
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../mocks/RandomGenerator_mock.h"
+#include "../../../src/utils/TimeProvider.h"
+
+static const StudySettings* ss = StudySettings::inst();
+
+const double SRModel_pickCard_Test::HourAgo = -1. / 24;
+
+void SRModel_pickCard_Test::SetUp()
+{
+ randomGenerator->setRand(1);
+}
+
+// Time interval
+
+TEST_F(SRModel_pickCard_Test, pick_active1)
+{
+ addStudied(-1, 1);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_active2)
+{
+ addStudied(-8.5, 8.1);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_active_and_future)
+{
+ addStudied(-1, 1);
+ addStudied(-1, 2);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2active1)
+{
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+
+ randomGenerator->setRand(0);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2active2)
+{
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_3active)
+{
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ addStudied(-1.2, 1);
+ testPicked({1, 2, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_future1)
+{
+ addStudied(-1, 2);
+ testPicked({NoCard});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_future2)
+{
+ addStudied(-1, 2);
+ addStudied(-1.1, 2);
+ testPicked({NoCard});
+}
+
+
+// Priority cards with small intervals
+// Always pick first cards with small interval
+
+TEST_F(SRModel_pickCard_Test, pick_zeroInterval) // Unreal
+{
+ addStudied(HourAgo, 0);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown)
+{
+ addUnknown(HourAgo);
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown_and_active)
+{
+ addUnknown(HourAgo);
+ addStudied(-1, 1);
+ testPicked({0, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_learning3min_and_active)
+{
+ addLearning(-3./(24*60)); // 3 min back
+ addStudied(-1, 1);
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_learning10min_and_active)
+{
+ addLearning(-10./(24*60));
+ addStudied(-1, 1);
+ testPicked({0, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_learningPrevDay_and_active)
+{
+ addLearning(-1);
+ addStudied(-1, 1);
+ testPicked({0, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown_and_2actives)
+{
+ addUnknown(HourAgo);
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ testPicked({0, 2, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remembered_and_2actives)
+{
+ addStudied(-1, ss->nextDayInterval);
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ testPicked({0, 2, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_competing_unknown_and_new)
+{
+ addUnknown(HourAgo);
+ addNew();
+ addStudied(-1, 1);
+ testPicked({0, 1, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_competing_remembered_and_new)
+{
+ addStudied(-1, ss->nextDayInterval);
+ addNew();
+ addStudied(-1, 1);
+ testPicked({0, 1, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2unknowns_and_2actives)
+{
+ addUnknown(HourAgo);
+ addUnknown(2 * HourAgo);
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ testPicked({1, 0, 3, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown_and_3actives)
+{
+ addUnknown(HourAgo);
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+ addStudied(-1.2, 1);
+ testPicked({0, 2, 3, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown_and_remembered)
+{
+ addUnknown(HourAgo);
+ addStudied(-1, ss->nextDayInterval);
+ addStudied(-1, 1);
+ testPicked({0, 1, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_incorrect_and_remembered)
+{
+ addIncorrect(HourAgo);
+ addStudied(-1, ss->nextDayInterval);
+ addStudied(-1, 1);
+ testPicked({0, 1, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_unknown_and_incorrect)
+{
+ addUnknown(HourAgo);
+ addIncorrect(HourAgo);
+ addStudied(-1, 1);
+ testPicked({0, 1, 2});
+}
+
+// New cards
+
+TEST_F(SRModel_pickCard_Test, pick_new)
+{
+ addNew();
+ testPicked({0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_new_and_studied1)
+{
+ addNew();
+ addStudied(-1.1, 1);
+
+ randomGenerator->setDouble(0.3); // Pick studied first
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_new_and_studied2)
+{
+ addNew();
+ addStudied(-1.1, 1);
+
+ // Pick new first
+ testPicked({0, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2new)
+{
+ addNew();
+ addNew();
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_3new)
+{
+ for(int i = 0; i < 3; i++)
+ addNew();
+ testPicked({1, 2, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2new_and_2studied1)
+{
+ addNew();
+ addNew();
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+
+ randomGenerator->setDouble(0.3); // Pick studied first
+ testPicked({3, 2, 1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2new_and_2studied2)
+{
+ addNew();
+ addNew();
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+
+ // Pick new first, double random = 0
+ testPicked({1, 0, 3, 2});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2new_and_2studied3)
+{
+ addNew();
+ addNew();
+ addStudied(-1, 1);
+ addStudied(-1.1, 1);
+
+ randomGenerator->setDouble(0.3); // Pick studied first
+ model.testPickCard();
+ assertCurCard(3);
+ randomGenerator->setDouble(0.1); // Pick new first
+ model.scheduleCard(4);
+ assertCurCard(1);
+ randomGenerator->setDouble(0.3);
+ model.scheduleCard(4);
+ assertCurCard(2);
+ randomGenerator->setDouble(0.1);
+ model.scheduleCard(4);
+ assertCurCard(0);
+}
+
+TEST_F(SRModel_pickCard_Test, pick_incorrect_new_and_studied)
+{
+ addIncorrect(HourAgo);
+ addNew();
+ addStudied(-1, 1);
+
+ randomGenerator->setDouble(0.3); // Pick studied first
+ testPicked({0, 2, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_newAndActive_newLimitReached)
+{
+ for(int i = 0; i < ss->newCardsDayLimit; i++)
+ addStudied(HourAgo, ss->nextDayInterval);
+
+ addNew();
+ addStudied(-1, 1);
+
+ int studiedCard = ss->newCardsDayLimit + 1;
+ testPicked({studiedCard, NoCard});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_2new_newLimitReached)
+{
+ for(int i = 0; i < ss->newCardsDayLimit; i++)
+ addStudied(HourAgo, ss->nextDayInterval);
+
+ addNew();
+ addNew();
+
+ testPicked({NoCard});
+}
+
+// Remaining incorrect cards
+// Must be taken in the end (both active and new cards are finished)
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_unknown)
+{
+ addUnknown(Now);
+ addStudied(-1, 1);
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_incorrect)
+{
+ addIncorrect(Now);
+ addStudied(-1, 1);
+ testPicked({1, 0});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_remembered)
+{
+ addStudied(Now, ss->nextDayInterval);
+ addStudied(-1, 1);
+ testPicked({1, NoCard});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_unknownAndIncorrect)
+{
+ addIncorrect(HourAgo);
+ addUnknown(Now);
+ addStudied(-1, 1);
+ testPicked({0, 2, 1});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_after_new)
+{
+ addNew();
+ testPicked({0, 0, 0, NoCard});
+}
+
+TEST_F(SRModel_pickCard_Test, pick_remaining_after_2new)
+{
+ addNew();
+ addNew();
+ testPicked({1, 0, 1, 0, 1, 0, NoCard});
+}
+
+
+
+QDateTime SRModel_pickCard_Test::timeFromDelta(double daysDelta)
+{
+ return TimeProvider::get().addSecs((int)(daysDelta * 24 * 60 * 60));
+}
+
+void SRModel_pickCard_Test::addStudied(double daysDelta, double interval,
+ int grade, int level)
+{
+ QString name = addRecord();
+ pack.generateQuestions();
+ StudyRecord study(level, grade, 2.5, interval);
+ study.date = timeFromDelta(daysDelta);
+ pack.addStudyRecord(name, study);
+}
+
+void SRModel_pickCard_Test::addUnknown(double daysDelta)
+{
+ addStudied(daysDelta, ss->unknownInterval, StudyRecord::Unknown,
+ StudyRecord::ShortLearning);
+}
+
+void SRModel_pickCard_Test::addIncorrect(double daysDelta)
+{
+ addStudied(daysDelta, ss->incorrectInterval, StudyRecord::Incorrect,
+ StudyRecord::ShortLearning);
+}
+
+void SRModel_pickCard_Test::addLearning(double daysDelta)
+{
+ addStudied(daysDelta, ss->learningInterval, StudyRecord::Good,
+ StudyRecord::LongLearning);
+}
+
+void SRModel_pickCard_Test::addNew()
+{
+ addRecord();
+ pack.generateQuestions();
+}
+
+void SRModel_pickCard_Test::assertCurCard(int expCard)
+{
+ if(expCard < 0)
+ {
+ ASSERT_FALSE(model.getCurCard());
+ return;
+ }
+ ASSERT_TRUE(model.getCurCard());
+ ASSERT_EQ(expCard, model.getCurCard()->getQuestion().toInt());
+}
+
+void SRModel_pickCard_Test::testPicked(const vector<int>& expCards)
+{
+ vector<int> pickedCards;
+ for(unsigned i = 0; i < expCards.size(); i++)
+ {
+ if(i == 0)
+ model.testPickCard();
+ else
+ model.scheduleCard(StudyRecord::Good);
+ if(model.getCurCard())
+ pickedCards.push_back(model.getCurCard()->getQuestion().toInt());
+ else
+ pickedCards.push_back(-1);
+ }
+ ASSERT_EQ(expCards, pickedCards);
+}
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.h b/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.h
new file mode 100644
index 0000000..aacb59a
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.h
@@ -0,0 +1,27 @@
+#include "SRModel_test.h"
+
+#include <vector>
+
+using namespace std;
+
+class SRModel_pickCard_Test: public SRModelTest
+{
+public:
+ static const int NoCard = -1;
+ static const int Now = 0;
+ static const double HourAgo;
+
+protected:
+ static QDateTime timeFromDelta(double daysDelta);
+
+protected:
+ void SetUp();
+ void addStudied(double daysDelta, double interval, int grade = 4,
+ int level = StudyRecord::Repeating);
+ void addUnknown(double daysDelta);
+ void addIncorrect(double daysDelta);
+ void addLearning(double daysDelta);
+ void addNew();
+ void assertCurCard(int expCard);
+ void testPicked(const vector<int>& expCards);
+};
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.cpp b/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.cpp
new file mode 100644
index 0000000..cd65154
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.cpp
@@ -0,0 +1,304 @@
+#include "SRModel_schedule_test.h"
+#include "../../../src/utils/TimeProvider.h"
+
+const double SRModel_schedule_Test::startE = StudySettings().initEasiness;
+
+void SRModel_schedule_Test::SetUp()
+{
+ StudySettings::inst()->schedRandomness = StudySettings().schedRandomness;
+ randomGenerator->setDouble(0.3);
+ cardName = createCard();
+}
+
+static const StudySettings* ss = StudySettings::inst();
+
+
+// New
+// Grades 1, 2, 3 not used
+
+TEST_F(SRModel_schedule_Test, new_good) // Levels: 1 -> 2
+ {
+ setNewCard();
+ schedule(4);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, new_easy) // Levels: 1 -> 3
+ {
+ setNewCard();
+ schedule(5);
+ checkStudy(StudyRecord::LongLearning, startE, ss->learningInterval);
+ }
+
+
+// Short Learning
+
+TEST_F(SRModel_schedule_Test, short_unknown) // same level
+ {
+ setStudy({StudyRecord::ShortLearning, 4, startE, ss->unknownInterval});
+ schedule(1);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, short_incor) // same level with longer interval
+ {
+ setStudy({StudyRecord::ShortLearning, 4, startE, ss->unknownInterval});
+ schedule(2);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->incorrectInterval);
+ }
+
+ // grade 3 not used
+
+TEST_F(SRModel_schedule_Test, short_good) // Levels: 2 -> 3
+ {
+ setStudy({StudyRecord::ShortLearning, 4, startE, ss->unknownInterval});
+ schedule(4);
+ checkStudy(StudyRecord::LongLearning, startE, ss->learningInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, short_easy) // Levels: 2 -> repeat next day
+ {
+ setStudy({StudyRecord::ShortLearning, 4, startE, ss->unknownInterval});
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, startE, ss->nextDayInterval);
+ }
+
+
+// Short learning from previous day:
+// Is promoted directly to next day repeating
+
+TEST_F(SRModel_schedule_Test, short_prevDay_difficult)
+ {
+ setStudy({StudyRecord::ShortLearning, 4, 2.5, ss->unknownInterval}, -1);
+ schedule(3);
+ checkStudy(StudyRecord::Repeating, 2.36, 2.18772);
+ }
+
+TEST_F(SRModel_schedule_Test, short_prevDay_good)
+ {
+ setStudy({StudyRecord::ShortLearning, 4, startE, ss->unknownInterval}, -1);
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, startE, 2.3175);
+ }
+
+TEST_F(SRModel_schedule_Test, short_prevDay_easy)
+ {
+ setStudy({StudyRecord::ShortLearning, 4, 2.5, ss->unknownInterval}, -1);
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, 2.6, 2.4102);
+ }
+
+
+// Long Learning
+
+TEST_F(SRModel_schedule_Test, long_unknown) // Levels: 3 -> 2
+ {
+ setStudy({StudyRecord::LongLearning, 4, startE, ss->learningInterval});
+ schedule(1);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, long_incor) // Levels: 3 -> 2
+ {
+ setStudy({StudyRecord::LongLearning, 4, startE, ss->learningInterval});
+ schedule(2);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->incorrectInterval);
+ }
+
+ // grade 3 not used
+
+TEST_F(SRModel_schedule_Test, long_good) // Levels: 3 -> repeat next day
+ {
+ setStudy({StudyRecord::LongLearning, 4, startE, ss->learningInterval});
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, startE, ss->nextDayInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, long_easy) // Levels: 3 -> repeat in 2 days
+ {
+ setStudy({StudyRecord::LongLearning, 4, startE, ss->learningInterval});
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, startE, ss->twoDaysInterval);
+ }
+
+
+// Long learning from previous day:
+// Is promoted directly to next day repeating
+
+TEST_F(SRModel_schedule_Test, long_prevDay_difficult)
+ {
+ setStudy({StudyRecord::LongLearning, 4, 2.5, ss->learningInterval}, -1);
+ schedule(3);
+ checkStudy(StudyRecord::Repeating, 2.36, 2.18772);
+ }
+
+TEST_F(SRModel_schedule_Test, long_prevDay_good)
+ {
+ setStudy({StudyRecord::LongLearning, 4, startE, ss->learningInterval}, -1);
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, startE, 2.3175);
+ }
+
+TEST_F(SRModel_schedule_Test, long_prevDay_easy)
+ {
+ setStudy({StudyRecord::LongLearning, 4, 2.5, ss->learningInterval}, -1);
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, 2.6, 2.4102);
+ }
+
+
+// First repeating day
+
+TEST_F(SRModel_schedule_Test, firstRep_unknown) // -> 2
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, ss->nextDayInterval});
+ schedule(1);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, firstRep_incor) // -> 2
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, ss->nextDayInterval});
+ schedule(2);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->incorrectInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, firstRep_difficult)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, ss->nextDayInterval});
+ schedule(3);
+ checkStudy(StudyRecord::Repeating, 2.36, 2.18772);
+ }
+
+TEST_F(SRModel_schedule_Test, firstRep_good)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, ss->nextDayInterval});
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, startE, 2.3175);
+ }
+
+TEST_F(SRModel_schedule_Test, firstRep_easy)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, ss->nextDayInterval});
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, 2.6, 2.4102);
+ }
+
+
+// Normal repeating
+
+TEST_F(SRModel_schedule_Test, repeat_unknown)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, 5});
+ schedule(1);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, repeat_incor)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, 5});
+ schedule(2);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->incorrectInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, repeat_difficult)
+ {
+ setStudy({StudyRecord::Repeating, 4, 2.5, 5});
+ schedule(3);
+ checkStudy(StudyRecord::Repeating, 2.36, 12.1540);
+ }
+
+TEST_F(SRModel_schedule_Test, repeat_good)
+ {
+ setStudy({StudyRecord::Repeating, 4, startE, 5});
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, startE, 12.875);
+ }
+
+TEST_F(SRModel_schedule_Test, repeat_easy)
+ {
+ setStudy({StudyRecord::Repeating, 4, 2.5, 5});
+ schedule(5);
+ checkStudy(StudyRecord::Repeating, 2.6, 13.39);
+ }
+
+
+// Mature repeating
+
+TEST_F(SRModel_schedule_Test, mature_difficult)
+ {
+ setStudy({StudyRecord::Repeating, 4, 3.1, 23});
+ schedule(3);
+ checkStudy(StudyRecord::Repeating, 2.96, 70.1224);
+ }
+
+TEST_F(SRModel_schedule_Test, mature_good)
+ {
+ setStudy({StudyRecord::Repeating, 4, 3.1, 23});
+ schedule(4);
+ checkStudy(StudyRecord::Repeating, 3.1, 73.439);
+ }
+
+
+// Re-learning unknown and incorrect cards
+
+TEST_F(SRModel_schedule_Test, unknown_incor)
+ {
+ setStudy({StudyRecord::ShortLearning, 1, startE, ss->unknownInterval});
+ schedule(2);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->incorrectInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, incor_unknown)
+ {
+ setStudy({StudyRecord::ShortLearning, 2, startE, ss->incorrectInterval});
+ schedule(1);
+ checkStudy(StudyRecord::ShortLearning, startE, ss->unknownInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, unknown_good)
+ {
+ setStudy({StudyRecord::ShortLearning, 1, startE, ss->unknownInterval});
+ schedule(4);
+ checkStudy(StudyRecord::LongLearning, startE, ss->learningInterval);
+ }
+
+TEST_F(SRModel_schedule_Test, incor_good)
+ {
+ setStudy({StudyRecord::ShortLearning, 2, startE, ss->incorrectInterval});
+ schedule(4);
+ checkStudy(StudyRecord::LongLearning, startE, ss->learningInterval);
+ }
+
+
+void SRModel_schedule_Test::setNewCard()
+{
+ pack.addStudyRecord(cardName, StudyRecord());
+}
+
+void SRModel_schedule_Test::setStudy(StudyRecord study, double daysDelta)
+{
+ study.date = timeFromDelta(daysDelta);
+ pack.addStudyRecord(cardName, study);
+}
+
+QDateTime SRModel_schedule_Test::timeFromDelta(double daysDelta)
+{
+ return TimeProvider::get().addSecs((int)(daysDelta * 24 * 60 * 60));
+}
+
+void SRModel_schedule_Test::schedule(int grade)
+{
+ this->grade = grade;
+ model.scheduleCard(grade);
+}
+
+void SRModel_schedule_Test::checkStudy(int expLevel, double expEasiness,
+ double expInterval)
+{
+ StudyRecord newStudy = pack.getStudyRecord(cardName);
+ ASSERT_EQ(grade, newStudy.grade);
+ ASSERT_EQ(expLevel, newStudy.level);
+ ASSERT_DOUBLE_EQ(expEasiness, newStudy.easiness);
+ ASSERT_NEAR(expInterval, newStudy.interval, 0.00000001);
+}
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.h b/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.h
new file mode 100644
index 0000000..6da4365
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_schedule_test.h
@@ -0,0 +1,24 @@
+#include "SRModel_test.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../../src/study/StudySettings.h"
+#include "../../mocks/RandomGenerator_mock.h"
+
+class SRModel_schedule_Test: public SRModelTest
+{
+protected:
+ static const double startE;
+
+protected:
+ void SetUp();
+ void setNewCard();
+ void setStudy(StudyRecord study, double daysDelta = 0);
+ void schedule(int grade);
+ void checkStudy(int expLevel, double expEasiness, double expInterval);
+
+private:
+ static QDateTime timeFromDelta(double daysDelta);
+
+protected:
+ QString cardName;
+ int grade;
+};
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.cpp b/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.cpp
new file mode 100644
index 0000000..2fd0675
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.cpp
@@ -0,0 +1,36 @@
+#include "SRModel_showGrades_test.h"
+
+static const StudySettings* ss = StudySettings::inst();
+
+void SRModel_showGrades_Test::SetUp()
+{
+ StudySettings::inst()->schedRandomness = StudySettings().schedRandomness;
+ randomGenerator->setDouble(0.3);
+ cardName = createCard();
+}
+
+TEST_F(SRModel_showGrades_Test, new)
+ {
+ setStudy({StudyRecord::New, 4, ss->initEasiness, 0});
+ QList<int> expGrades = {4, 5};
+ ASSERT_EQ(expGrades, model.getAvailableGrades());
+ }
+
+TEST_F(SRModel_showGrades_Test, learning)
+ {
+ setStudy({StudyRecord::ShortLearning, 4, ss->initEasiness, ss->unknownInterval});
+ QList<int> expGrades = {1, 2, 4, 5};
+ ASSERT_EQ(expGrades, model.getAvailableGrades());
+ }
+
+TEST_F(SRModel_showGrades_Test, repeating)
+ {
+ setStudy({StudyRecord::Repeating, 4, ss->initEasiness, 2});
+ QList<int> expGrades = {1, 2, 3, 4, 5};
+ ASSERT_EQ(expGrades, model.getAvailableGrades());
+ }
+
+void SRModel_showGrades_Test::setStudy(const StudyRecord& study)
+{
+ pack.addStudyRecord(cardName, study);
+}
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.h b/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.h
new file mode 100644
index 0000000..c45a760
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.h
@@ -0,0 +1,14 @@
+#include "SRModel_test.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../../src/study/StudySettings.h"
+#include "../../mocks/RandomGenerator_mock.h"
+
+class SRModel_showGrades_Test: public SRModelTest
+{
+protected:
+ void SetUp();
+ void setStudy(const StudyRecord& study);
+
+protected:
+ QString cardName;
+};
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_test.cpp b/tests/unit/SpacedRepetitionModel/SRModel_test.cpp
new file mode 100644
index 0000000..420dfa7
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_test.cpp
@@ -0,0 +1,67 @@
+#include "SRModel_test.h"
+#include "../../common/printQtTypes.h"
+#include "../../../src/dictionary/DicRecord.h"
+#include "../../../src/dictionary/Card.h"
+#include "../../../src/study/StudySettings.h"
+#include "../../mocks/RandomGenerator_mock.h"
+
+SRModelTest::SRModelTest():
+ recordId(0),
+ pack(&dict),
+ randomGenerator(new MockRandomGenerator),
+ model(&pack, randomGenerator)
+{
+ field1 = new Field("English", "Normal");
+ field2 = new Field("Russian", "Normal");
+ pack.addField(field1);
+ pack.addField(field2);
+ dict.addCardPack(&pack);
+}
+
+void SRModelTest::SetUp()
+{
+ QCoreApplication::setOrganizationName("freshmemory");
+ QCoreApplication::setApplicationName("freshmemory");
+ QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, "");
+ StudySettings::inst()->load();
+}
+
+void SRModelTest::TearDown()
+{
+ delete field1;
+ delete field2;
+}
+
+TEST_F(SRModelTest, EmptyStats)
+ {
+ ASSERT_EQ(0, model.estimatedNewReviewedCardsToday());
+ ASSERT_EQ(0, model.countTodayRemainingCards());
+ }
+
+TEST_F(SRModelTest, FirstStudy)
+ {
+ QString cardName = createCard();
+ StudyRecord newStudy = pack.getStudyRecord(cardName);
+
+ ASSERT_EQ(StudyRecord::Unknown, newStudy.grade);
+ ASSERT_DOUBLE_EQ(StudySettings::inst()->initEasiness, newStudy.easiness);
+ ASSERT_DOUBLE_EQ(0, newStudy.interval);
+ }
+
+QString SRModelTest::createCard()
+{
+ addRecord();
+ pack.generateQuestions();
+ return model.getCurCard()->getQuestion();
+}
+
+QString SRModelTest::addRecord()
+{
+ DicRecord* record = new DicRecord;
+ QString recordName = QString::number(recordId);
+ record->setField("English", recordName);
+ record->setField("Russian", "Odin");
+ recordId++;
+ dict.addRecord(record);
+ return recordName;
+}
diff --git a/tests/unit/SpacedRepetitionModel/SRModel_test.h b/tests/unit/SpacedRepetitionModel/SRModel_test.h
new file mode 100644
index 0000000..9e5d669
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/SRModel_test.h
@@ -0,0 +1,46 @@
+#ifndef SPACED_REPETITION_MODEL_TEST_H
+#define SPACED_REPETITION_MODEL_TEST_H
+
+#include <gtest/gtest.h>
+#include <iostream>
+#include <QtCore>
+
+#include "../../../src/study/SpacedRepetitionModel.h"
+#include "../../../src/dictionary/CardPack.h"
+#include "../../mocks/Dictionary_mock.h"
+
+using std::ostream;
+
+class MockRandomGenerator;
+class Field;
+
+class TestSpacedRepetitionModel: public SpacedRepetitionModel
+{
+public:
+ TestSpacedRepetitionModel(CardPack* pack, IRandomGenerator* random):
+ SpacedRepetitionModel(pack, random) {}
+ void testPickCard() { pickNextCardAndNotify(); }
+};
+
+class SRModelTest: public testing::Test
+{
+public:
+ SRModelTest();
+
+protected:
+ void SetUp();
+ void TearDown();
+ QString createCard();
+ QString addRecord();
+
+protected:
+ MockDictionary dict;
+ int recordId;
+ Field* field1;
+ Field* field2;
+ CardPack pack;
+ MockRandomGenerator* randomGenerator;
+ TestSpacedRepetitionModel model;
+};
+
+#endif
diff --git a/tests/unit/SpacedRepetitionModel/srModel.pri b/tests/unit/SpacedRepetitionModel/srModel.pri
new file mode 100644
index 0000000..19daca1
--- /dev/null
+++ b/tests/unit/SpacedRepetitionModel/srModel.pri
@@ -0,0 +1,16 @@
+HEADERS += \
+ $$PWD/SRModel_test.h \
+ $$PWD/SRModel_schedule_test.h \
+ $$PWD/SRModel_pickCard_test.h \
+ $$PWD/SRModel_showGrades_test.h \
+ $$SRC/study/SpacedRepetitionModel.h \
+ $$SRC/study/IStudyModel.h \
+ $$TESTS/mocks/RandomGenerator_mock.h
+
+SOURCES += \
+ $$PWD/SRModel_test.cpp \
+ $$PWD/SRModel_schedule_test.cpp \
+ $$PWD/SRModel_pickCard_test.cpp \
+ $$PWD/SRModel_showGrades_test.cpp \
+ $$SRC/study/SpacedRepetitionModel.cpp \
+ $$SRC/study/IStudyModel.cpp
diff --git a/tests/unit/cards.pri b/tests/unit/cards.pri
new file mode 100644
index 0000000..e3c563e
--- /dev/null
+++ b/tests/unit/cards.pri
@@ -0,0 +1,24 @@
+HEADERS += \
+ $$SRC/dictionary/Card.h \
+ $$SRC/dictionary/ICardPack.h \
+ $$SRC/dictionary/CardPack.h \
+ $$SRC/dictionary/IDictionary.h \
+ $$SRC/dictionary/Field.h \
+ $$SRC/dictionary/DicRecord.h \
+ $$SRC/study/StudyRecord.h \
+ $$SRC/utils/TimeProvider.h \
+ $$TESTS/mocks/Dictionary_mock.h \
+ $$TESTS/common/RecordsParam.h
+
+SOURCES += \
+ $$SRC/dictionary/Card.cpp \
+ $$SRC/dictionary/ICardPack.cpp \
+ $$SRC/dictionary/CardPack.cpp \
+ $$SRC/dictionary/IDictionary.cpp \
+ $$SRC/dictionary/Field.cpp \
+ $$SRC/dictionary/DicRecord.cpp \
+ $$SRC/study/StudyRecord.cpp \
+ $$TESTS/mocks/Dictionary_mock.cpp \
+ $$TESTS/mocks/TimeProvider_mock.cpp \
+ $$TESTS/common/RecordsParam.cpp \
+ $$TESTS/common/RecordsParam_create.cpp
diff --git a/tests/unit/common.pri b/tests/unit/common.pri
new file mode 100644
index 0000000..482dab7
--- /dev/null
+++ b/tests/unit/common.pri
@@ -0,0 +1,3 @@
+SOURCES += \
+ main.cpp \
+ $$TESTS/common/printQtTypes.cpp
diff --git a/tests/unit/main.cpp b/tests/unit/main.cpp
new file mode 100644
index 0000000..eb8f2b9
--- /dev/null
+++ b/tests/unit/main.cpp
@@ -0,0 +1,9 @@
+#include <gtest/gtest.h>
+#include <QtWidgets>
+
+int main(int argc, char** argv)
+{
+ QApplication app(argc, argv);
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/tests/unit/random.pri b/tests/unit/random.pri
new file mode 100644
index 0000000..003c663
--- /dev/null
+++ b/tests/unit/random.pri
@@ -0,0 +1 @@
+HEADERS += $$SRC/utils/IRandomGenerator.h
diff --git a/tests/unit/studySets.pri b/tests/unit/studySets.pri
new file mode 100644
index 0000000..50c6bd1
--- /dev/null
+++ b/tests/unit/studySets.pri
@@ -0,0 +1,9 @@
+HEADERS += \
+ $$SRC/field-styles/FieldStyleFactory.h \
+ $$SRC/field-styles/FieldStyle.h \
+ $$SRC/study/StudySettings.h
+
+SOURCES += \
+ $$SRC/field-styles/FieldStyleFactory.cpp \
+ $$SRC/field-styles/FieldStyle.cpp \
+ $$SRC/study/StudySettings.cpp \ No newline at end of file
diff --git a/tests/unit/unit_tests.pro b/tests/unit/unit_tests.pro
new file mode 100644
index 0000000..49f74d0
--- /dev/null
+++ b/tests/unit/unit_tests.pro
@@ -0,0 +1,29 @@
+TEMPLATE = app
+TARGET = unittest
+QT += widgets
+LIBS += -lgtest -lgtest_main
+QMAKE_CXXFLAGS += -std=gnu++11
+DESTDIR = ./
+
+win32: {
+ QMAKE_LIBS_QT_ENTRY =
+ CONFIG += console
+ }
+
+MOC_DIR = moc
+OBJECTS_DIR = obj
+
+SRC = $$PWD/../../src
+TESTS = $$PWD/..
+
+include(common.pri)
+include(cards.pri)
+include(studySets.pri)
+include(random.pri)
+
+include(Card/card.pri)
+include(CardPack/cPack.pri)
+include(CardSideView/csView.pri)
+include(RandomGenerator/rndGen.pri)
+include(Settings/set.pri)
+include(SpacedRepetitionModel/srModel.pri)
diff --git a/tr/freshmemory_cs.ts b/tr/freshmemory_cs.ts
new file mode 100644
index 0000000..a09f493
--- /dev/null
+++ b/tr/freshmemory_cs.ts
@@ -0,0 +1,1505 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="cs_CZ">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>O %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Licence:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Učte se novým věcem rychle a udržte svou paměť čerstvou časově rozloženým opakováním.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Není otevřen žádný slovník.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Nynější slovník je prázdný.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Jít na okno se slovníkem</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Zavřít</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Upravit kartu: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(prádný balíček)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Náhled karty</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Náhled:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Oddělovače</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation>Ta&amp;b</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>Oddělovací znak &amp;textu:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Vyvést do CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>Zapsat &amp;názvy sloupců</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Z&amp;naková sada:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Použité &amp;sloupce:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Výstup</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>&amp;Dát všechna pole do uvozovek</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Oddělovač pole:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Znak pro &amp;poznámku:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Ukázat &amp;neviditelné znaky</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Nelze uložit do souboru:
+%1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Zavést z CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Z&amp;naková sada:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>Od řá&amp;dku:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>Počet sloup&amp;ců:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Vše</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>V &amp;prvním řádku jsou názvy polí</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Vstup</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Jakýkoliv znak</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>&amp;Kombinace znaků</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>Přesné řetě&amp;zec</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Znak pro &amp;poznámku:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Oddělovač pole:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Režim oddělení:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>Bez názvu</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Otázka</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Odpověď</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Příklad</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Nelze otevřít soubor slovníku:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Nelze otevřít učební soubor:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Soubor není slovníkovým souborem.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Nepodporovaný formát</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Starý slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Soubor není učebním souborem.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Učební soubor používá nepodporovaný formát %1.
+Nejnižší podporovaá verze je %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Název souboru</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Volby pro slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Karetní balíčky</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Styl</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>Nové pole</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Posunout nahoru</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Posunout dolů</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Přidat</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Přidat nové pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Odstranit</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Odstranit pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Přejmenovat</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Přejmenovat pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Náhled</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Zavřít</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Hledat:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Najít předchozí</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Najít další</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation>Rozlišovat velká a malá písmena</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Celá slova</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation>Regulární výraz</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>Ve výběru</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Řetězec nenalezen</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Nastavení písma a barev</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Barva pozadí karty:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Styly polí</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Rodina písma:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Velikost:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Tučné</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Kurzíva</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Barva:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Předpona:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Přípona:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Styl klíčového slova</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Náhled stylu</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Zavřít tento balíček</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation>E</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Upravit kartu</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation>D</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Smazat kartu</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Ukázat odpověď</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Smazat kartu?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Smazat kartu &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Jazyk</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Systému</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>Aplikace musí být restartován, aby mohli používat zvolený jazyk</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Záznamy: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Otevřít slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Uložit slovník jako...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Zavést soubor CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Vyvést jako soubor CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Nelze uložit slovník:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Nelze uložit učební soubor:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Uložit slovník?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Slovník %1 byl změněn.
+Uložit změny?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Nový</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation>Ctrl+N</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Otevřít...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation>Ctrl+O</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Uložit</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation>Ctrl+S</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Uložit &amp;jako...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Uložit &amp;kopii...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Zavést z CSV...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Vyvést do CSV...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>&amp;Zavřít slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation>Ctrl+W</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Ukončit</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation>Ctrl+Q</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Kopírovat</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation>Ctrl+C</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>Vyj&amp;mout</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation>Ctrl+X</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>&amp;Vložit</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation>Ctrl+V</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>S&amp;tatistika</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>Nastavení &amp;písma a barev</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Nápověda</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>O aplikaci</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>Učení nelze začít.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation>Ctrl+I</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>&amp;Hledat...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation>Ctrl+F</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Hledat &amp;znovu</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>Prohlížení &amp;slov</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Slovníky</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Přidat obrázek</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Obrázky</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Všechny soubory</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Rozložené opakování</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>Volby pro &amp;slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>Nastavení &amp;učení</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Soubor</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>&amp;Nedávné soubory</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>Úp&amp;ravy</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation>Ctrl+Z</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation>Ctrl+Y</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>Přidat &amp;obrázek</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>&amp;Vložit záznam</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>&amp;Odstranit záznam</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Pohled</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>Ná&amp;stroje</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>&amp;Volby</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Nápověda</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation>Hlavní</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Balíčky karet</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation>
+ <numerusform>Vložené záznamy obsahují %n nové pole</numerusform>
+ <numerusform>Vložené záznamy obsahují %n nová pole</numerusform>
+ <numerusform>Vložené záznamy obsahují %n nových polí</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation>Chcete přidat nová pole do tohoto slovníku?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation>Přidat nová pole</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation>Vložit pouze existující pole</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Stránky:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation>Použití:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation>SOUBOR je souborový název slovníku k nahrání.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation>Volby:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation>Zobrazit tuto nápovědu a ukončit</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation>Vypsat údaje o verzi a ukončit</translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Přidat</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Odstranit</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Posunout balíček nahoru</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Posunout balíček dolů</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Balíčky karet</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Posunout pole nahoru</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Posunout pole dolů</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Pole v balíčcích</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Odstranit pole z balíčku</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Přidat pole do balíčku</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Nepoužitá pole</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Náhled</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Balíček karet</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Rozvrh</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Nový</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation type="unfinished">Učeno</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation type="unfinished">Nový</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Celkem: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Vložit %n záznam</numerusform>
+ <numerusform>Vložit %n záznamy</numerusform>
+ <numerusform>Vložit %n záznamů</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Odstranit %n záznam</numerusform>
+ <numerusform>Odstranit %n záznamy</numerusform>
+ <numerusform>Odstranit %n záznamů</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Upravit &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Vložit %n záznam</numerusform>
+ <numerusform>Vložit %n záznamy</numerusform>
+ <numerusform>Vložit %n záznamů</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Zařazené karty</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Rozložené opakování</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation type="unfinished">Zařazené karty</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Všechny karty jsou zopakovány</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Můžete jít na další balíček nebo slovník, nebo otevřít Prohlížení slov.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Statistika</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Balíček karet:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Perioda:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n týden</numerusform>
+ <numerusform>%n týdny</numerusform>
+ <numerusform>%n týdnů</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n měsíc</numerusform>
+ <numerusform>%n měsíce</numerusform>
+ <numerusform>%n měsíců</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n rok</numerusform>
+ <numerusform>%n roky</numerusform>
+ <numerusform>%n roků</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Celou dobu</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Autor: Mykhaylo Kopytonenko</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Sestavení</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation>Fresh Memory</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Chyba</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Učené karty</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Podíl nových karet:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Denní nejvyšší počet nových karet:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Nastavení učení</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>Klíčové slovo</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation type="unfinished">Datum</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation type="unfinished">Karty</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Celkem: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Vytvořit nový slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Otevřít existující slovník</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Zavést z CSV souboru</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Nedávné slovníky</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Prohlížení slov</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation>Nynější karta/Všechny karty</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation>Postup opakování karet</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation>U</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Zpět</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation>Jít zpět v historii</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Vpřed</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation>Jít vpřed v historii</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation>Ukázat další kartu (Enter)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Další</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Ukázat odpovědi</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Nejsou dostupné žádné karty</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_de.ts b/tr/freshmemory_de.ts
new file mode 100644
index 0000000..d196b10
--- /dev/null
+++ b/tr/freshmemory_de.ts
@@ -0,0 +1,1501 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="de_DE">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>Über %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Lizenz:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Lernen Sie neues schnell und halten Sie Ihr Gedächtnis frisch mit der Wiederholung in bestimmten Abständen.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Kein Wörterbuch eröffnet.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Das aktuelle Wörterbuch ist leer.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Zum Wörterbuch Fenster</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Schließen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Bearbeiten Karte: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(leer Stapel)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Kartenvorschau</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Vorschau:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Trennzeichen</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>&amp;Texttrennzeichen:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Export nach CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>&amp;Spaltennamen Schreiben</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Zeichensatz:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Gebrauchte &amp;Spalten:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Ausgabe</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>Alle &amp;Felde zitieren</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Feldtrennzeichen:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>&amp;Kommentarzeichen:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>&amp;Unsichtbare Zeichen zeigen</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Kann nicht auf Datei zu speichern: %1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Import von CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Zeichensatz:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>Von Zei&amp;le:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>Anzahl der &amp;Spalten:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Alle</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>Es gibt &amp;Feldnamen in der erste Zeile</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Eingabe</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Jedes Zeichen</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>Eine &amp;Kombination von Zeichens</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>&amp;Genaue Zeichenfolge</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>&amp;Kommentarzeichen:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Feldtrennzeichen:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Trennungsmodus:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>kein_Name</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Frage</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Antwort</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Beispiel</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Kann nicht geöffnet werden Wörterbuchdatei:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Kann nicht geöffnet Studiedatei:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Die Datei ist keine Wörterbuchdatei.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Nicht unterstützten Format</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Das Wörterbuch verwendet nicht unterstütztes Format %1.
+Die minimale unterstützte Version ist %2</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation>Das Wörterbuch %1 verwendet veraltete Format %2.
+Es wird in das aktuelle Format %3 umgewandelt werden.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Alten Wörterbuch</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Die Datei ist keine Studiedatei.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Die Studie Datei verwendet nicht unterstütztes Format %1.
+Die minimale unterstützte Version ist %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Wörterbuch Optionen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Dateiname</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Felder</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Kartenstapels</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Feld</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Stil</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>neues Feld</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Felder</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Bewegen nach oben</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Bewegen nach unten</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Neues Feld hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Löschen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Felder löschen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Umbenennen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Feld umbenennen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Vorschau</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Schließen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Suchen:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Zurücksuchen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Weitersuchen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Ganze Wörter</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>in Auswahl</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Zeichenfolge nicht gefunden</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Font &amp; Farbeinstellungen</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Karten Hintergrundfarbe:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Feldstile</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Schriftart:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Größe:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Fett</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Kursiv</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Farbe:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Präfix:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Suffix:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Schlüsselwortstil</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Stilvorschau</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Dieses Stapel schließen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Karte bearbeiten</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Karte löschen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Antwort zeigen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Löschen Karte?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Löschen Karte &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Sprache</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Systems</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>Die Anwendung muss neu gestartet werden, um die ausgewählte Sprache zu verwenden</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Sätze: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Wörterbuch öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Wörterbücher</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Speichern Wörterbuch unter ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Import CSV-datei</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Export nach CSV-datei</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Kann nicht Wörterbuch zu speichern:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Kann nicht Studiedatei zu speichern:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Speichern Wörterbuch?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Wörterbuch %1 wurde geändert.
+Änderungen speichern?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Bild hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Bilder</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Alle Dateien</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation>Online Wörterbücher</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>&amp;Wörterbetrachtungs</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>&amp;Wörterbuchoptionen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>&amp;Font und Farbeinstellungen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>&amp;Studieeinstellungen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Neu</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>Ö&amp;ffnen ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Speichern</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Speichern &amp;unter ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>&amp;Kopie speichern ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Import von CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Export nach CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>Wörterbuch s&amp;chließen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Beenden</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Kopieren</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>&amp;Ausschneiden</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>&amp;Einfügen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>&amp;Suchen...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>&amp;Wieder suchen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>&amp;Bild hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>Satz ein&amp;fügen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>Satz &amp;löschen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>S&amp;tatistik</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Hilfe</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>Info</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Bearbeiten</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Ansicht</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>&amp;Werkzeuge</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>&amp;Optionen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Hilfe</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Datei</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>Ne&amp;ueste Dateien</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Kartenstapels</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>Die Studie kann nicht gestartet werden.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Hinzufügen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Löschen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Stapel bewegen nach oben</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Stapel bewegen nach unten</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Kartenstapels</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Feld bewegen nach oben</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Stapel bewegen nach unten</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Stapelfelder</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Feld löschen vom Stapel</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Feld hinzufügen zu Stapel</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation>Nutzt genaue Antwort</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Ungenutzte Felder</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Vorschau</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Kartenstapel</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Geplan</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Neu</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation>Studierte</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation>Geplante für heute</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation>Neue</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation>Studienfortschritt</translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>%n Satz einfügen</numerusform>
+ <numerusform>%n Sätze einfügen</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>%n Satz löschen</numerusform>
+ <numerusform>%n Sätze löschen</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>&quot;%1&quot; bearbeiten</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>%n Satz einfügen</numerusform>
+ <numerusform>%n Sätze einfügen</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Geplante karten</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation>Heute gelernt neue Karten</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation>Geplante Lernbewertungen:
+neue Karten müssen heute wiederholt werden, um zu erfahren</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation>Geplante Karten für heute</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation>Neue geplanten Karten für heute:
+neue Karten, die zwischen den geplanten Karten gezeigt werden</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation>Angesehen Karten</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation>Lernbewertungen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation>Geplante Karten</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation>Neue Karten für heute</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation>Fortschritt der Bewertungen für heute:</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation>Unbekannt</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation>Völlig vergessen Karte, konnte nicht die Antwort wieder zu verwenden.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation>Falsch</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation>Die Antwort ist falsch.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation>Schwer</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation>Es ist schwierig, die Antwort zu erinnern. Das letzte Intervall zu lang war.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation>Gut</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation>Die Antwort wird in wenigen Sekunden zurückgerufen. Das letzte Intervall war gut genug.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation>Einfach</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation>Die Karte ist zu einfach, und erinnerte daran, ohne jede Anstrengung. Das letzte Intervall zu kurz war.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation>(%1 Min)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Alle Karten werden angesehen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Sie können auf den nächst Stapel oder Wörterbuch zu gehen, oder Wörterbetrachtungs zu öffnen.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Statistik</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Kartenstapel:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Zeitraum:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n Jahr</numerusform>
+ <numerusform>%n Jahre</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n Woche</numerusform>
+ <numerusform>%n Wochen</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n Monat</numerusform>
+ <numerusform>%n Monate</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Die ganze Zeit</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Bau</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Verfasser: Mykhaylo Kopytonenko</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Fehler</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Studierte Karten</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation>Fügen neue Karten in zufälliger Reihenfolge</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation>Tag beginnt um, Uhr:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Anteil neuer Karten:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation>Wiederholungsintervall Zufälligkeit:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation>Tage-Grenze auf Bewertungen:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation>Nicht hinzufügen neue Karten nach dem geplanten Karten Schwelle:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation>Grenzen</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Tage-Grenze auf neuer Karten:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Studieeinstellungen</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>schlüsselwort</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation>Datum</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation>Karten</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Neues Wörterbuch erstellen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Bestehenden Wörterbuch öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation>Online wörterbücher öffnen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Import von CSV-datei</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Neueste Wörterbucher</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Wörterbetrachtungs</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Antworten zeigen</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Zurück</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Vorwärts</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Weiter</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Keine Karten</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_en.ts b/tr/freshmemory_en.ts
new file mode 100644
index 0000000..5640b39
--- /dev/null
+++ b/tr/freshmemory_en.ts
@@ -0,0 +1,1495 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>The application must be restarted to use the selected language</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation>
+ <numerusform>The pasted records contain %n new field</numerusform>
+ <numerusform>The pasted records contain %n new fields</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Insert %n record</numerusform>
+ <numerusform>Insert %n records</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Remove %n record</numerusform>
+ <numerusform>Remove %n records</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Paste %n record</numerusform>
+ <numerusform>Paste %n records</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n week</numerusform>
+ <numerusform>%n weeks</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n month</numerusform>
+ <numerusform>%n months</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n year</numerusform>
+ <numerusform>%n years</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_es.ts b/tr/freshmemory_es.ts
new file mode 100644
index 0000000..7e4af77
--- /dev/null
+++ b/tr/freshmemory_es.ts
@@ -0,0 +1,1502 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="es_ES">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>Acerca de %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Licencia:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Aprender cosas nuevas rápidamente y mantener su memoria fresca con la repaso espaciado.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>No diccionario abrió.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>El diccionario actual está vacía.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Ir a ventana del diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Cerrar</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Editar tarjeta: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(paquete vacío)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Previsualización de tarjeta</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Previsualización:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Separadores</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>Delimitador de &amp;texto:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Exportar a CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>&amp;Escribir los nombres de columnas</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>C&amp;onjunto de caracteres:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Colu&amp;mnas utilizadas:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Salida</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>Ci&amp;tar todos los campos</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>Sepa&amp;rador de campos:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Carácter de co&amp;mentario:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Mostrar caracteres &amp;invisibles</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>No se puede guardar archivo:
+ %1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Importar de CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>C&amp;onjunto de caracteres:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>A partir de &amp;línea:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>Número de &amp;columnas:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Todos</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>&amp;Primera línea tiene nombres de campos</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Entrada</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>Cual&amp;quier carácter</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>Una co&amp;mbinación de caracteres</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>Cadena e&amp;xacta</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Carácter de co&amp;mentario:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>Sepa&amp;rador de campos:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Modo de separación:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>sin_nombre</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Pregunta</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Respuesta</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Ejemplo</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>No se puede abrir archivo de diccionario:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>No se puede abrir archivo de estudio:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Este archivo no es un archivo de diccionario.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Formato no compatible</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>El diccionario utiliza el formato no compatible %1.
+La versión mínima compatible es %2</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation>El diccionario %1 utiliza formato obsoleto %2.
+Se convierte en el formato actual %3.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Viejo diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Este archivo no es un archivo de estudio.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>El archivo de estudio utiliza el formato no compatible %1.
+La versión mínima compatible es %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Configuración de diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Nombre del archivo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Campos</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Paquetes de tarjetas</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Campo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Estilo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>neuvo campo</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Campos</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Arriba</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Abajo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Añadir</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Añadir nuevo campo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Eliminar</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Eliminar campos</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Cambiar nombre</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Cambiar nombre de campo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Previsualización</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Cerrar</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Buscar:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Buscar anterior</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Buscar siguiente</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Palabras completas</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>En selección</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Cadena no se encuentra</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Configuración de fuente y color</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Color de fondo de tarjeta:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Estilos de campos</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Tipo de fuente:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Tamaño:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Negrita</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Cursiva</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Color:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Prefijo:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Sufijo:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Estilo de palabra clave</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Previsualización de estilo</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Cerrar este paquete</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Editar tarjeta</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Eliminar tarjeta</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Mostrar respuesta</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Eliminar tarjeta?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Eliminar tarjeta &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Idioma</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>De sistema</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>La aplicación debe ser reiniciado para utilizar el idioma seleccionado</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Registros: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Abrir diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Diccionarios</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Guardar diccionario como ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Importar CSV archivo</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Exportar a CSV archivo</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>No se puede guardar diccionario:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>No se puede guardar archivo de estudio:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Guardar diccionario?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Diccionario %1 se modificó.
+Guardar los cambios?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Agregar imagen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Imágenes</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Todos archivos</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation>Online diccionarios</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>Examen de &amp;palabras</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Repaso espaciado</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>Configuración de &amp;diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>Configuración de &amp;fuente y color</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>Configuración de &amp;estudio</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Nuevo</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Abrir ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Guardar</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Guardar &amp;como ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Guardar &amp;copia ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Importar de CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Exportar a CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>Ce&amp;rrar diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>Sa&amp;lir</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Copiar</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>C&amp;ortar</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>&amp;Pegar</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>&amp;Buscar...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Buscar s&amp;iguiente</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>&amp;Agregar imagen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>&amp;Insertar registro</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>E&amp;liminar registro</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>Es&amp;tadística</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Ayuda</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>Acerca de</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Edición</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Ver</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>&amp;Herramientas</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>&amp;Configuración</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Ayuda</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Archivo</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>Archivos &amp;recientes</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Paquetes de tarjetas</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>El estudio no se puede iniciar.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Sitio web:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Añadir</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Eliminar</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Paquete arriba</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Paquete abajo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Paquetes de tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Campo arriba</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Campo abajo</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Campos de paquete</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Eliminar campo de paquete</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Añadir campo a paquete</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation>Utiliza respuesta exacta</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Campos no utilizados</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Previsualización</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Paquete de tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Progr</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Nuev</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation>Estudiadas</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation>Programadas para hoy</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation>Nuev</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation>Progreso del estudio</translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Insertar %n registro</numerusform>
+ <numerusform>Insertar %n registros</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Eliminar %n registro</numerusform>
+ <numerusform>Eliminar %n registros</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Editar &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Pegar %1 registro</numerusform>
+ <numerusform>Pegar %1 registros</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Tarjetas programadas</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Repaso espaciado</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation>Hoy aprendió nuevas tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation>Programadas opiniones de aprendizaje:
+nuevas tarjetas deben repetirse hoy para aprender</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation>Tarjetas programadas para hoy</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation>Nuevas tarjetas programadas para hoy:
+nuevas tarjetas que se mostrarán entre las programadas tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation>Vistas tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation>Aprendiendas revistas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation>Tarjetas programadas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation>Nuevas tarjetas para hoy</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation>El progreso de los exámenes programados para hoy:</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation>Desconocido</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation>Completamente tarjeta olvidado, no podía recordar la respuesta.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation>Incorrecto</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation>La respuesta es incorrecta.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation>Difícil</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation>Es difícil recordar la respuesta. El último intervalo era demasiado largo.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation>Bien</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation>La respuesta se recordó en un par de segundos. El último intervalo era lo suficientemente bueno.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation>Fácil</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation>La tarjeta es demasiado fácil, y recordó sin ningún esfuerzo. El último intervalo era demasiado corto.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation>Aceptar</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation>(%1 min)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Todas las tarjetas se vistas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Usted puede ir a la siguiente paquete o diccionario, o abrir Examen de palabras.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Estadística</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Paquete de tarjetas:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Período:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n año</numerusform>
+ <numerusform>%n años</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n semana</numerusform>
+ <numerusform>%n semanas</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n mes</numerusform>
+ <numerusform>%n meses</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Todo el tiempo</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Compilación</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Autor: Mykhaylo Kopytonenko</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Error</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Tarjetas estudiadas</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation>Añadir nuevas tarjetas en orden aleatorio</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation>Día comienza a las:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Proporción de nuevas tarjetas:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation>Aleatoriedad de intervalo de repetición:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation>Límite de día de revistas:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation>No agregue nuevas tarjetas después umbral de tarjetas programadas:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation>Limites</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Límite de día de tarjetas nuevas:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Configuración de estudio</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>palabra clave</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation>Fecha</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation>Tarjetas</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Crear nuevo diccionario</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Abrir diccionario existente</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation>Abrir online diccionarios</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Importar de CSV archivo</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Diccionarios recientes</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Examen de palabras</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Mostrar respuestas</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Atrás</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Adelante</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Sigulente</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>No hay tarjetas</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_fi.ts b/tr/freshmemory_fi.ts
new file mode 100644
index 0000000..4096244
--- /dev/null
+++ b/tr/freshmemory_fi.ts
@@ -0,0 +1,1498 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fi_FI">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>Tietoja sovelluksesta %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Lisenssi:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Opi uutta nopeasti ja pidä muistisi tuoreena väliaikakertauksen avulla.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Ei avattu mitään sanakirjaa.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Nykyinen sanakirja on tyhjä.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Mene sanakirjan ikkunaan</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Sulje</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Kortin muokkaaminen: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(tyhjä pakka)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Kortin esimäkymä</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Esinäkymä:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Jakajat</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation>Sar&amp;k</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>&amp;Tekstin rajoitin:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Vie CSV-tidostoon</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>Kirjoita sarakkeiden &amp;nimet</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Merkistö:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Käytetyt &amp;sarakkeet:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Ulostulo</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>&amp;Siteeraa kaikki kentät</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Kenttäjakaja:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Ko&amp;mmenttimerkki:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Näytä &amp;näkymättömiä merkkejä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Ei voida tallentaa tiedostoon:
+%1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Tuo CSV-tiedostosta</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Merkistö:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>&amp;Riviltä:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>Sarakkeiden &amp;määrä:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Kaikki</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>&amp;Ensimmäisellä rivillä on kenttänimiä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Sisääntulo</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Mikä tahansa merkki</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation>Kenttiä erotetaan millä tahansa merkillä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>Merkki&amp;yhdistelmä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation>Kenttiä erotetaan merkkiyhdistelmällä, missä tahansa järjestyksessä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>&amp;Tarkka merkkijono</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation>Kenttiä erotetaan tarkalla merkkijonolla, edellä mainitussa järjestyksessä</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Ko&amp;mmenttimerkki:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Kenttäjakaja:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Erottamisen tila:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>nimetön</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Kysymys</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Vastaus</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Esimerkki</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Ei voida avata sanakirjan tiedostoa:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Ei voida avata oppimistiedostoa:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Tiedosto ei ole sanakirjan tiedosto.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Tukematon formaatti</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Vanha sanakirja</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Tiedosto ei ole oppimistiedosto.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Oppimistiedosto käyttää tukematonta formaattia %1.
+Alin tuettu versio on %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Tiedostonimi</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Sanakirjan asetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Kentät</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Korttipakat</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Kenttä</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Tyyli</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>uusi kenttä</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Kentät</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Siirrä ylös</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Siirrä alas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Lisää</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Lisää uusi kenttä</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Poista</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Poista kenttiä</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Nimeä uudelleen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Nimeä kenttä uudelleen</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Esinäkymä</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Sulje</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Hae:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Etsi edeellinen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Etsi seuraava</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation>Kirjainkoolla on merkitystä</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Kokonaiset sanat</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation>Säännöllinen lauseke</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>Valituilla</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Merkkijono ei etsitty</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Fontti- ja väriasetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Kortin taustaväri:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Kenttätyylit</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Fonttinimi:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Koko:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Lihava</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Kursiivi</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Väri:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Etuliite:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Pääte:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Avainsanan tyyli</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Tyylin esinäkymä</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Sulje tämä pakka</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Muokkaa kortti</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Poista kortti</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Näytä vastaus</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Poista kortti?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Poista kortti &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Kieli</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Järjestelmän</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>Ohjelman pitää käynnistää uudelleen että se käyttäisi valittua kieltä</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Tietueita: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Avaa sanakirja</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Tallenna sanakirja nimellä ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Tuo CSV-tiedostosta</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Vie CSV-tiedostoon</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Ei voida tallentaa sanakirjaa:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Ei voida tallentaa oppimistiedostoa:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Tallentaa sanakirjan?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Sanakirja %1 oli muutettu.
+Tallentaa muutokset?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Uusi</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Avaa ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Tallenna</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Tallenna &amp;nimellä ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Tallenna &amp;kopio ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>T&amp;uo CSV-tiedostosta ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Vie CSV-tiedostoon ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>&amp;Sulje sanakirja</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Lopeta</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Kopioi</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>&amp;Leikkaa</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>Lii&amp;tä</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>S&amp;tatistiikka</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>&amp;Fontti- ja väriasetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Ohje</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>Tietoja sovelluksesta</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>Oppiminen ei voida alkaa.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>&amp;Etsi ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Etsi &amp;uudelleen</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>&amp;Sanojen harjoittelu</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Sanakirjat</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Lisää kuva</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Kuvat</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Kaikki tiedostot</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Aikavälikertaus</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>&amp;Sanakirjan asetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>&amp;Oppimisasetukset</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Tiedosto</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>Avaa &amp;uusin</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Muokkaa</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>Lisää &amp;kuva</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>&amp;Lisää tietue</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>&amp;Poista tietue</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Näytä</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>&amp;Työkalut</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>&amp;Valinnat</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Ohje</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation>Pääpalkki</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Korttipakat</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation>
+ <numerusform>Liitetyillä tietueilla on %n uusi kenttä</numerusform>
+ <numerusform>Liitetyillä tietueilla on %n uutta kenttää</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation>Haluatko lisätä uusia kenttiä tähän sanakirjaan?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation>Lisää uusia kenttiä</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation>Liitä vain olemassa olevia kenttiä</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Web-sivut:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation>Käyttö:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation>FILE on ladattava sanakirjan tiedostonimi.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation>Valinnat:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation>Näytä tämä ohje ja lopeta</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation>Tulosta version tiedot ja lopeta</translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Lisää</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Poista</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Siirrä pakka ylös</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Siirrä pakka alas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Korttipakat</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Siirrä kenttä ylös</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Siirrä kenttä alas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Pakan kentät</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Poista kenttä pakasta</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Lisää kenttä pakkaan</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Käyttämättömät kentät</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Esinäkymä</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Korttipakka</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Suunn</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Uudet</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation type="unfinished">Opitut</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation type="unfinished">Uudet</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Yhteensä: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Lisää %n tietue</numerusform>
+ <numerusform>Lisää %n tietuetta</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Poista %n tietue</numerusform>
+ <numerusform>Poista %n tietuetta</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Muokkaa &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Littä %n tietue</numerusform>
+ <numerusform>Littä %n tietuetta</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Suunnitelleet kortit</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Aikavälikertaus</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation type="unfinished">Suunnitelleet kortit</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Kaikki kortit ovat selattuja</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Voit siirtää seuraavaan pakkaan tai sanakirjaan, taikka avata Sanojen harjoittelu.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Statistiikka</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Korttipakka:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Aikaväli:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n viikko</numerusform>
+ <numerusform>%n viikkoa</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n kuukausi</numerusform>
+ <numerusform>%n kuukautta</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n vuosi</numerusform>
+ <numerusform>%n vuotta</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Koko aika</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Tekijä: Mykhaylo Kopytonenko</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Muutos</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation>Fresh Memory</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Virhe</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Opitut kortit</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Uusien korttien osuus:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Uusien korttien päiväraja:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Oppimisen asetukset</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>avainsana</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation type="unfinished">Päivämäärä</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation type="unfinished">Kortteja</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Yhteensä: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Luo uusi sanakirja</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Avaa olemassa oleva sanakirja</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Tuo CSV-tiedostosta</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Uusimmat sanakirjat</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Sanojen harjoittelu</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation>Nykyinen kortti / Kaikki kortit</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation>Korttien selaamisen tila</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Takaisin</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation>Palata takaisin historiassa</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Eteenpäin</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation>Siirrä eteenpäin historiassa</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation>Näytä seuraava kortti (Enter)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Seuraava</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Näytä vastaukset</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Ei ole kortteja</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_fr.ts b/tr/freshmemory_fr.ts
new file mode 100644
index 0000000..0f6c06e
--- /dev/null
+++ b/tr/freshmemory_fr.ts
@@ -0,0 +1,1500 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fr_FR">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>A propos %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Licence:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Apprendre de nouvelles choses rapidement et de garder votre mémoire fraîche avec répétition espacée.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Aucun dictionnaire ouvert.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Le dictionnaire courant est vide.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Aller à fenêtre de dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Fermer</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Éditer la carte: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(paquet vide)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Prévisualisation de carte</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Prévisualisation:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Séparateurs</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation>Ta&amp;b</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>Séparateur de &amp;texte:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Exporter au CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>Écrire &amp;noms de colonnes</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Jeu de caractères:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>&amp;Colonnes utilisées:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Sortie</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>Ci&amp;ter tous les champs</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Séparateur de champs:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Co&amp;mmentaire caractère:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Afficher les caractères &amp;invisibles</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Impossible d&apos;enregistrer fichier:
+ %1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Importer de CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>&amp;Jeu de caractères:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>De &amp;ligne:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>&amp;Nombre de colonnes:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Toutes</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>&amp;Première ligne contient des noms de champs</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Apport</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Tout caractère</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>Une co&amp;mbinaison de caractères</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>Chaîne e&amp;xacte</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Co&amp;mmentaire caractère:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Séparateur de champs:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Mode de séparation:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>pas_de_nom</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Question</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Réponse</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Exemple</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Impossible d&apos;ouvrir le fichier dictionnaire:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Impossible d&apos;ouvrir le fichier d&apos;étude:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Le fichier est pas un fichier dictionnaire.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Format non supporté</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Le dictionnaire utilise le format non supporté %1.
+La version minimum supportée est de %2</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation>Le dictionnaire %1 utilise format obsolète %2.
+Il sera converti au format actuel %3.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Vieux dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Le fichier est pas un fichier d&apos;étude.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Le fichier d&apos;étude utilise le format non supporté %1.
+La version minimum supportée est de %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Paramètres de dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Nom du fichier</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Champs</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Paquets de cartes</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Champ</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Style</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>nouveau champ</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Champs</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Vers le Haut</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Vers le Bas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Ajouter</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Supprimer</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Renommer</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Prévisualisation</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Fermer</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Rechercher:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Suivant</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Mots complets</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>En sélection</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Chaîne est introuvable</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Paramètres de police et couleur</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Couleur de fond de carte:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Styles de champs</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Famille de police:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Taille:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Gras</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Italique</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Couleur:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Préfixe:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Suffixe:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Style mot-clé</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Prévisualisation de style</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Fermer ce paquet</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Éditer carte</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Supprimer carte</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Afficher réponse</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Supprimer carte?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Supprimer carte &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Langue</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Du système</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>L&apos;application doit être redémarré pour utiliser la langue sélectionnée</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Enregistrements: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Ouvrir dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Dictionnaires</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Enregistrer dictionnaire sous ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Importer CSV fichier</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Exporter au CSV fichier</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Impossible d&apos;enregistrer dictionnaire:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Impossible d&apos;enregistrer fichier d&apos;étude:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Enregistrer dictionnaire?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Dictionnaire %1 a été modifié.
+Enregistrer les modifications?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Ajouter image</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Images</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Tous fichiers</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation>Online dictionnaires</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>Examen de &amp;mots</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Répétition espacée</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>Paramètres de &amp;dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>Paramètres de &amp;police et couleur</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>&amp;Paramètres de étude</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Nouveau</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Ouvrir ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>Enre&amp;gistrer</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Enregistrer so&amp;us ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Enregistrer &amp;copie ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Importer depuis CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Exporter au CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>&amp;Fermer dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>Sor&amp;tir</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Copier</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>Co&amp;uper</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>Co&amp;ller</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>&amp;Rechercher...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Suiv&amp;ant</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>&amp;Ajouter image</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>&amp;Insérer enregistrement</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>Supp&amp;rimer enregistrement</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>S&amp;tatistique</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Aide</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>A propos</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Édition</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>A&amp;ffichage</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>Ou&amp;tils</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>&amp;Paramètres</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Aide</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Fichier</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>Fichiers &amp;récents</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Paquets de cartes</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>L&apos;étude ne peut pas être démarré.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Site web:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Ajouter</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Supprimer</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Paquet vers le haut</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Paquet vers le bas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Paquets de cartes</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Champ vers le haut</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Champ vers le bas</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Champs de paquet</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Champs inutilisés</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Prévisualisation</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Paquet de cartes</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Plan</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Nouv</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation>Étudiées</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation>Planifiées pour aujourd&apos;hui</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation>Nouv</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation>Progrès de l&apos;étude</translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Insérer %n enregistrement</numerusform>
+ <numerusform>Insérer %n enregistrements</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Supprimer %n enregistrement</numerusform>
+ <numerusform>Supprimer %n enregistrements</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Éditer &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Coller %n enregistrement</numerusform>
+ <numerusform>Coller %n enregistrements</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Cartes planifiées</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Répétition espacée</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation>Cartes planifiées</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation>Inconnu</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation>Incorrecte</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation>Difficile</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation>Bien</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation>Facile</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation>(%1 min)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Toutes les cartes sont examinées</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Vous pouvez aller à le meute paquet ou un dictionnaire, ou ouvrir l&apos;Examen de mots.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Statistique</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Paquet de cartes:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Période:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n semaine</numerusform>
+ <numerusform>%n semaines</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n mois</numerusform>
+ <numerusform>%n mois</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n an</numerusform>
+ <numerusform>%n années</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Tout le temps</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Construcion</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Auteur: Mykhaylo Kopytonenko</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Erreur</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Сartes étudiées</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation>Ajouter nouvelles cartes dans un ordre aléatoire</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation>Journée commence à:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Partager de nouvelles cartes:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation>Aléatoire de Intervalle de répétition:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation>Limite de jour pour répétitions:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation>Ne pas ajouter de nouvelles cartes après seuil des cartes programmées:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation>Limites</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Limite jour de nouvelles cartes:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Paramètres de étude</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>mot-clé</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation>Date</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation>Cartes</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation>Total: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Créer nouveau dictionnaire</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Ouvrir dictionnaire existant</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation>Ouvrir dictionnaires online</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Importer de CSV fichier</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Dictionnaires récents</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Examen de mots</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Afficher réponses</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Précédent</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Avant</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Suivant</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Pas de cartes</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_ru.ts b/tr/freshmemory_ru.ts
new file mode 100644
index 0000000..b1981ce
--- /dev/null
+++ b/tr/freshmemory_ru.ts
@@ -0,0 +1,1507 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="ru_RU">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>О программе %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Лицензия:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Изучайте новое быстро и поддерживайте память методом интервальных повторений.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Не открыт ни один словарь.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Текущий словарь пуст.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Перейти к словарю</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Закрыть</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Редактирование карточки: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(пустая колода)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Просмотр карточки</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Просмотр:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Разделители</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation>Та&amp;б</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>О&amp;граничитель текста:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Экспорт в CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>Вписать &amp;имена колонок</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Кодиро&amp;вка:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Используемые &amp;колонки:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Вывод</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>Заключить в к&amp;авычки все поля</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Разделитель полей:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Символ ко&amp;мментария:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Показать &amp;невидимые символы</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Невозможно сохранить в файл:\n %1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Импорт из CSV файла</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Кодиро&amp;вка:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>С&amp;о строки:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>&amp;Количество колонок:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Все</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>В &amp;первой строке имена полей</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Ввод</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Любой символ</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation>Поля разделяются любым символом-разделителем</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>&amp;Комбинация символов</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation>Поля разделяются комбинацией символов-разделителей, в любом порядке</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>&amp;Определенная строка</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation>Поля разделяются в точности определенной строкой из разделителей, в вышеуказанном порядке</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Символ &amp;комментария:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Разделитель полей:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Режим разделения:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>безымянный</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Вопрос</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Ответ</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Пример</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Невозможно открыть файл словаря:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Невозможно открыть файл обучения:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Файл не является словарем.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Неподдерживаемый формат</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Словарь использует неподдерживаемый формат %1.
+Минимальная поддерживаемая версия - %2</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation>Словарь %1 использует устаревший формат %2.
+Он будет преобразован в текущий формат %3.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Старый словарь</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Этот файл не является файлом обучения.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Файл обучения использует неподдерживаемый формат %1.\nМинимальная поддерживаемая версия - %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Имя файла</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Параметры словаря</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Колоды карточек</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Стиль</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>новое поле</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Передвинуть вверх</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Передвинуть вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Добавить</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Добавить новое поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Удалить</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Удалить поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Переименовать</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Переименовать поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Просмотр</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Закрыть</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Поиск:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Найти предыдущую</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Найти следующую</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation>Учитывать регистр символов</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Слова целиком</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation>Регулярное выражение</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>В выбранных</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Строка не найдена</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Настройки шрифта и цвета</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Цвет фона карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Стили полей</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Шрифт:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Размер:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Жирный</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Курсив</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Цвет:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Префикс:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Суффикс:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Стиль ключевого слова</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Просмотр стилей</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Закрыть эту колоду</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Редактировать карточку</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Удалить карточку</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Показать ответ</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Удалить карточку?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Удалить карточку &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Язык</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Системный</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>Нужно перезапустить приложение, чтобы использовать выбранный язык</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Записей: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Открыть словарь</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Сохранить словарь как ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Импорт из CSV файла</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Экспорт в CSV файл</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Сохранить словарь?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Словарь %1 изменен. Сохранить изменения?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Новый</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>&amp;Закрыть словарь</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Открыть ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Невозможно сохранить словарь:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Невозможно сохранить файл обучения:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Вставить картинку</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Картинки</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Все файлы</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation>Онлайн словари</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Сохранить</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Сохранить &amp;как...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Сохранить копи&amp;ю ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Импорт из CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Экспорт в CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Выход</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Копировать</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>Вы&amp;резать</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>&amp;Вставить из буфера</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>С&amp;татистика</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>Настройки &amp;шрифта и цвета</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Справка</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>О программе</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>Невозможно начать обучение.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>П&amp;оиск...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Найти с&amp;нова</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>&amp;Просмотр слов</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Словари</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Интервальное повторение</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>Настройки &amp;словаря</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>Настройки &amp;обучения</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Файл</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>&amp;Последние файлы</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Правка</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>&amp;Вставить картинку</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>&amp;Вставить запись</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>&amp;Удалить запись</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Вид</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>&amp;Инструменты</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>Пара&amp;метры</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Справка</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation>Главная</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Колоды карточек</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation>
+ <numerusform>Вставляемые записи содержат %n новое поле</numerusform>
+ <numerusform>Вставляемые записи содержат %n новых поля</numerusform>
+ <numerusform>Вставляемые записи содержат %n новых полей</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation>Вы хотите добавить новые поля в этот словарь?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation>Добавить новые поля</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation>Вставить только существующие поля</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Веб-сайт:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation>Использование:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation>FILE - имя загружаемого файла словаря.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation>Параметры:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation>Показать эту справку и выйти</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation>Вывести информацию о версии и выйти</translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Добавить</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Удалить</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Передвинуть колоду вверх</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Передвинуть колоду вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Колоды карточек</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Передвинуть поле вверх</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Передвинуть поле вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Поля колоды</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Удалить поле из колоды</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Добавть поле в колоду</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation>Использует точный ответ</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Неиспользованные поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Просмотр</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Колода карточек</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Заплан</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Нов</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation>Изученные</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation>Запланир. на сегодня</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation>Новые</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation>Всего: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation>Ход обучения</translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Вставить %n запись</numerusform>
+ <numerusform>Вставить %n записи</numerusform>
+ <numerusform>Вставить %n записей</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Удалить %n запись</numerusform>
+ <numerusform>Удалить %n записи</numerusform>
+ <numerusform>Удалить %n записей</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Редактировать &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Вставить из буфера %n запись</numerusform>
+ <numerusform>Вставить из буфера %n записи</numerusform>
+ <numerusform>Вставить из буфера %n записей</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Запланированные карточки</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Интервальное повторение</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation>Сегодня изученные новые карточки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation>Запланированные обучающие просмотры:
+новые карточки нужно повторить сегодня чтобы их выучить</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation>Оставшееся время до следующего обучающего просмотра</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation>Запланированные карточки на сегодня</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation>Новые запланированные карточки на сегодня:
+новые карточки, которые будут показаны между запланированными</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation>Просмотренные карточки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation>Обучающие просмотры</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation>Запланированные карточки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation>Новые карточки на сегодня</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation>Ход просмотра запланированных на сегодня карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation>Не знаю</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation>Совершенно забытая карточка, не могу вспомнить ответ.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation>Неправильно</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation>Ответ неправильный.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation>Трудно</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation>Трудно вспомнить ответ. Последний интервал был слишком длинным.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation>Хорошо</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation>Ответ вспоминается через пару секунд. Последний интервал был подходящим.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation>Легко</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation>Карточка слишком легкая, и вспоминается без усилий. Последний интервал был слишком коротким.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation>OK</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation>(%1 мин)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation>Достигнут дневной предел для карточек: %1 штук.
+Рекомендуется закончить изучение этого словаря.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Все карточки просмотрены</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Вы можете перейти к следующей колоде или словарю, или открыть Просмотр слов.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Статистика</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Колода карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Период:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n год</numerusform>
+ <numerusform>%n года</numerusform>
+ <numerusform>%n лет</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n неделя</numerusform>
+ <numerusform>%n недели</numerusform>
+ <numerusform>%n недель</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n месяц</numerusform>
+ <numerusform>%n месяца</numerusform>
+ <numerusform>%n месяцев</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Всё время</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Автор: Михаил Копитоненко</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Сборка</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Ошибка</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Изученные карточки</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation>Добавлять новые карточки в случайном порядке</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation>День начинается в, часов:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Доля новых карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation>Разброс интервала повторений:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation>Дневное ограничение для просмотров:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation>Не добавлять новых карточек после предела запланированных карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation>Ограничения</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Дневное ограничение для новых карточек:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Настройки обучения</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>ключевое слово</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation>Дата</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation>Карточки</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation>Всего: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Создать новый словарь</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Открыть существующий словарь</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation>Открыть онлайн словари</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Импорт из CSV файла</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Последние словари</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation>Текущая карточка / Все карточки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation>Состояние просмотра карточек</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation>Вернуться назад по истории</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Вперёд</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation>Пройти вперёд по истории</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation>Показать следующую карточку (Enter)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Далее</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Просмотр слов</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Показывать ответы</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Нет карточек</translation>
+ </message>
+</context>
+</TS>
diff --git a/tr/freshmemory_uk.ts b/tr/freshmemory_uk.ts
new file mode 100644
index 0000000..3fff2f9
--- /dev/null
+++ b/tr/freshmemory_uk.ts
@@ -0,0 +1,1504 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="uk_UA">
+<context>
+ <name>AboutDialog</name>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="9"/>
+ <source>About %1</source>
+ <translation>Про %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="25"/>
+ <source>License:</source>
+ <translation>Ліцензія:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AboutDialog.cpp" line="21"/>
+ <source>Learn new things quickly and keep your memory fresh with time spaced repetition.</source>
+ <translation>Вивчайте нове швидко і підтримуйте свою пам’ять методом інтервальних повторень.</translation>
+ </message>
+</context>
+<context>
+ <name>AppModel</name>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="166"/>
+ <source>No dictionary opened.</source>
+ <translation>Не відкритий жоден словник.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/AppModel.cpp" line="171"/>
+ <location filename="../src/main-view/AppModel.cpp" line="179"/>
+ <source>The current dictionary is empty.</source>
+ <translation>Поточний словник пустий.</translation>
+ </message>
+</context>
+<context>
+ <name>CardEditDialog</name>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="53"/>
+ <source>Go to dictionary window</source>
+ <translation>Перейти до словника</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="55"/>
+ <source>Close</source>
+ <translation>Закрити</translation>
+ </message>
+ <message>
+ <location filename="../src/study/CardEditDialog.cpp" line="67"/>
+ <source>Edit card: </source>
+ <comment>In title of card edit view</comment>
+ <translation>Редагування картки: </translation>
+ </message>
+</context>
+<context>
+ <name>CardPack</name>
+ <message>
+ <location filename="../src/dictionary/CardPack.cpp" line="120"/>
+ <source>(empty pack)</source>
+ <translation>(пуста колода)</translation>
+ </message>
+</context>
+<context>
+ <name>CardPreview</name>
+ <message>
+ <location filename="../src/main-view/CardPreview.cpp" line="15"/>
+ <source>Card preview</source>
+ <translation>Перегляд картки</translation>
+ </message>
+</context>
+<context>
+ <name>CsvDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="32"/>
+ <source>Preview:</source>
+ <translation>Перегляд:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="69"/>
+ <source>Separators</source>
+ <translation>Роздільники</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="89"/>
+ <source>Ta&amp;b</source>
+ <translation>Та&amp;б</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvDialog.cpp" line="106"/>
+ <source>&amp;Text delimiter:</source>
+ <translation>Обмежувач &amp;тексту:</translation>
+ </message>
+</context>
+<context>
+ <name>CsvExportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="13"/>
+ <source>Export to CSV</source>
+ <translation>Експорт в CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="47"/>
+ <source>Write column &amp;names</source>
+ <translation>Вписати &amp;імена стовпчиків</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="32"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Коду&amp;вання:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="33"/>
+ <source>Used &amp;columns:</source>
+ <translation>Використані &amp;стовпчики:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.h" line="24"/>
+ <source>Output</source>
+ <translation>Вивід</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="54"/>
+ <source>&amp;Quote all fields</source>
+ <translation>Помістити всі поля в &amp;лапки</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="57"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Роздільник полів:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="60"/>
+ <source>Co&amp;mment character:</source>
+ <translation>Символ ко&amp;ментаря:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="83"/>
+ <source>Show &amp;invisible characters</source>
+ <translation>Показати &amp;невидимі символи</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvExportDialog.cpp" line="125"/>
+ <source>Cannot save to file:
+ %1.</source>
+ <translation>Неможливо зберегти у файл:
+%1.</translation>
+ </message>
+</context>
+<context>
+ <name>CsvImportDialog</name>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="15"/>
+ <source>Import from CSV</source>
+ <translation>Імпорт із CSV</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="40"/>
+ <source>C&amp;haracter set:</source>
+ <translation>Коду&amp;вання:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="41"/>
+ <source>From &amp;line:</source>
+ <translation>&amp;З рядка:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="42"/>
+ <source>Number of colum&amp;ns:</source>
+ <translation>Кількість &amp;стовпчиків:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="58"/>
+ <source>All</source>
+ <translation>Всі</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="64"/>
+ <source>&amp;First line has field names</source>
+ <translation>У &amp;першому рядку імена полів</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.h" line="28"/>
+ <source>Input</source>
+ <translation>Ввід</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="87"/>
+ <source>An&amp;y character</source>
+ <translation>&amp;Будь-який символ</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="88"/>
+ <source>Fields are separated by any separator character</source>
+ <translation>Поля розділяються будь-яким символом-роздільником</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="89"/>
+ <source>A co&amp;mbination of characters</source>
+ <translation>&amp;Комбінація символів</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="90"/>
+ <source>Fields are separated by a combination of separator characters, in any order</source>
+ <translation>Поля розділяються комбінацією символів-роздільників, у будь-якому порядку</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="92"/>
+ <source>E&amp;xact string</source>
+ <translation>То&amp;чний рядок</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="93"/>
+ <source>Fields are separated by the exact string of separators, in the above defined order</source>
+ <translation>Поля розділяються точно певним рядком із роздільників, у вищевказаному порядку</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="98"/>
+ <source>&amp;Comment character:</source>
+ <translation>Символ ко&amp;ментаря:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="74"/>
+ <source>Field &amp;separator:</source>
+ <translation>&amp;Роздільник полів:</translation>
+ </message>
+ <message>
+ <location filename="../src/export-import/CsvImportDialog.cpp" line="76"/>
+ <source>Separation mode:</source>
+ <translation>Режим розділення:</translation>
+ </message>
+</context>
+<context>
+ <name>Dictionary</name>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="21"/>
+ <source>noname</source>
+ <translation>безіменний</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="102"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="361"/>
+ <source>Question</source>
+ <translation>Питання</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="103"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="362"/>
+ <source>Answer</source>
+ <translation>Відповідь</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="104"/>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="363"/>
+ <source>Example</source>
+ <translation>Приклад</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="146"/>
+ <source>Cannot open dictionary file:</source>
+ <translation>Неможливо відкрити файл словника:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/Dictionary.cpp" line="185"/>
+ <source>Cannot open study file:</source>
+ <translation>Неможливо відкрити файл навчання:</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="30"/>
+ <source>The file is not a dictionary file.</source>
+ <translation>Файл не є словником.</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="65"/>
+ <location filename="../src/study/StudyFileReader.cpp" line="45"/>
+ <source>Unsupported format</source>
+ <translation>Формат не підтримується</translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="66"/>
+ <source>Dictionary uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="76"/>
+ <source>Dictionary %1 uses obsolete format %2.
+It will be converted to the current format %3.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dictionary/DictionaryReader.cpp" line="75"/>
+ <source>Old dictionary</source>
+ <translation>Старий словник</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="31"/>
+ <source>The file is not a study file.</source>
+ <translation>Файл не є файлом навчання.</translation>
+ </message>
+ <message>
+ <location filename="../src/study/StudyFileReader.cpp" line="46"/>
+ <source>The study file uses unsupported format %1.
+The minimum supported version is %2</source>
+ <translation>Файл навчання використовує формат %1, що не підтримується.
+Мінімальна версія, що підтримується, - %2</translation>
+ </message>
+</context>
+<context>
+ <name>DictionaryOptionsDialog</name>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="44"/>
+ <source>File name</source>
+ <translation>Ім’я файла</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="24"/>
+ <source>Dictionary options</source>
+ <translation>Параметри словника</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="58"/>
+ <source>Fields</source>
+ <translation>Поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/DictionaryOptionsDialog.cpp" line="59"/>
+ <source>Card packs</source>
+ <translation>Колоди карток</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsListModel</name>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="52"/>
+ <source>Field</source>
+ <translation>Поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="54"/>
+ <source>Style</source>
+ <translation>Стиль</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsListModel.cpp" line="94"/>
+ <source>new field</source>
+ <translation>нове поле</translation>
+ </message>
+</context>
+<context>
+ <name>FieldsPage</name>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="28"/>
+ <source>Fields</source>
+ <translation>Поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="33"/>
+ <source>Move up</source>
+ <translation>Пересунути вгору</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="37"/>
+ <source>Move down</source>
+ <translation>Пересунути вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="53"/>
+ <source>Add</source>
+ <translation>Додати</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="54"/>
+ <source>Add new field</source>
+ <translation>Додати нове поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="55"/>
+ <source>Remove</source>
+ <translation>Видалити</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="56"/>
+ <source>Remove field(s)</source>
+ <translation>Видалити поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="57"/>
+ <source>Rename</source>
+ <translation>Переіменувати</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="58"/>
+ <source>Rename field</source>
+ <translation>Переіменувати поле</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/FieldsPage.cpp" line="91"/>
+ <source>Preview</source>
+ <translation>Перегляд</translation>
+ </message>
+</context>
+<context>
+ <name>FindPanel</name>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="18"/>
+ <source>Close</source>
+ <translation>Закрити</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="20"/>
+ <source>Find:</source>
+ <comment>Title of the find pane</comment>
+ <translation>Знайти:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="34"/>
+ <source>Find previous</source>
+ <translation>Знайти попередній</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="40"/>
+ <source>Find next</source>
+ <translation>Знайти наступний</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="47"/>
+ <source>Case sensitive</source>
+ <translation>Враховувати регістр символів</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="53"/>
+ <source>Whole words</source>
+ <translation>Слова цілком</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="59"/>
+ <source>Regular expression</source>
+ <translation>Регулярний вираз</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="65"/>
+ <source>In selection</source>
+ <translation>У вибраних</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/FindPanel.cpp" line="70"/>
+ <source>String is not found</source>
+ <translation>Рядок не знайдено</translation>
+ </message>
+</context>
+<context>
+ <name>FontColorSettingsDialog</name>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="36"/>
+ <source>Font &amp; color settings</source>
+ <translation>Налаштування шрифта і кольору</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="52"/>
+ <source>Card background color:</source>
+ <translation>Колір фона карток:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="61"/>
+ <source>Field styles</source>
+ <translation>Стилі полей</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="91"/>
+ <source>Font family:</source>
+ <translation>Шрифт:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="94"/>
+ <source>Size:</source>
+ <translation>Розмір:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="103"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="142"/>
+ <source>Bold</source>
+ <translation>Жирний</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="104"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="143"/>
+ <source>Italic</source>
+ <translation>Курсив</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="106"/>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="149"/>
+ <source>Color:</source>
+ <translation>Колір:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="123"/>
+ <source>Prefix:</source>
+ <translation>Префікс:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="125"/>
+ <source>Suffix:</source>
+ <translation>Суфікс:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="137"/>
+ <source>Keyword style</source>
+ <translation>Стиль ключового слова</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/FontColorSettingsDialog.cpp" line="161"/>
+ <source>Style preview</source>
+ <translation>Перегляд стилей</translation>
+ </message>
+</context>
+<context>
+ <name>IStudyWindow</name>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="90"/>
+ <source>Close this pack</source>
+ <translation>Закрити цю колоду</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="114"/>
+ <source>E</source>
+ <comment>Shortcut for &apos;Edit card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="115"/>
+ <source>Edit card</source>
+ <translation>Редагувати картку</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="126"/>
+ <source>D</source>
+ <comment>Shortcut for &apos;Delete card&apos; button</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="127"/>
+ <source>Delete card</source>
+ <translation>Видалити картку</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="165"/>
+ <location filename="../src/study/IStudyWindow.cpp" line="168"/>
+ <source>Show answer</source>
+ <translation>Показати відповідь</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card?</source>
+ <translation>Видалити картку?</translation>
+ </message>
+ <message>
+ <location filename="../src/study/IStudyWindow.cpp" line="216"/>
+ <source>Delete card &quot;%1&quot;?</source>
+ <translation>Видалити картку &quot;%1&quot;?</translation>
+ </message>
+</context>
+<context>
+ <name>LanguageMenu</name>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="6"/>
+ <source>&amp;Language</source>
+ <translation>&amp;Мова</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="33"/>
+ <source>System</source>
+ <translation>Системна</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/LanguageMenu.cpp" line="63"/>
+ <source>The application must be restarted to use the selected language</source>
+ <translation>Треба перезапустити програму, щоб використовувати вибрану мову</translation>
+ </message>
+</context>
+<context>
+ <name>MainWindow</name>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="87"/>
+ <source>Records: %1</source>
+ <translation>Записів: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="169"/>
+ <source>Open dictionary</source>
+ <translation>Відкрити словник</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="290"/>
+ <source>Save dictionary as ...</source>
+ <translation>Зберегти словник як ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="310"/>
+ <source>Import CSV file</source>
+ <translation>Імпорт із CSV файла</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="335"/>
+ <source>Export to CSV file</source>
+ <translation>Експорт у CSV файл</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="348"/>
+ <source>Cannot save dictionary:</source>
+ <translation>Неможливо зберегти словник:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="368"/>
+ <source>Cannot save study file:</source>
+ <translation>Неможливо зберегти файл навчання:</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Save dictionary?</source>
+ <translation>Зберегти словник?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="413"/>
+ <source>Dictionary %1 was modified.
+Save changes?</source>
+ <translation>Словник %1 змінено. Зберегти зміни?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="609"/>
+ <source>&amp;New</source>
+ <translation>&amp;Новий</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="610"/>
+ <source>Ctrl+N</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="613"/>
+ <source>&amp;Open ...</source>
+ <translation>&amp;Відкрити ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="614"/>
+ <source>Ctrl+O</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="620"/>
+ <source>&amp;Save</source>
+ <translation>&amp;Зберегти</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="621"/>
+ <source>Ctrl+S</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="624"/>
+ <source>Save &amp;as ...</source>
+ <translation>Зберегти &amp;як ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="627"/>
+ <source>Save &amp;copy ...</source>
+ <translation>Зберегти &amp;копію ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="630"/>
+ <source>&amp;Import from CSV ...</source>
+ <translation>&amp;Імпорт із CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="633"/>
+ <source>&amp;Export to CSV ...</source>
+ <translation>&amp;Експорт в CSV ...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="637"/>
+ <source>&amp;Close dictionary</source>
+ <translation>&amp;Закрити словник</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="638"/>
+ <source>Ctrl+W</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="641"/>
+ <source>&amp;Quit</source>
+ <translation>&amp;Вихід</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="642"/>
+ <source>Ctrl+Q</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="654"/>
+ <source>&amp;Copy</source>
+ <translation>&amp;Копіювати</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="655"/>
+ <source>Ctrl+C</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="658"/>
+ <source>Cu&amp;t</source>
+ <translation>Ви&amp;різати</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="659"/>
+ <source>Ctrl+X</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="662"/>
+ <source>&amp;Paste</source>
+ <translation>&amp;Вставити</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="663"/>
+ <source>Ctrl+V</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="722"/>
+ <source>S&amp;tatistics</source>
+ <translation>С&amp;татистика</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="736"/>
+ <source>&amp;Font and color settings</source>
+ <translation>Налаштування &amp;шрифта і кольору</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="748"/>
+ <source>Help</source>
+ <translation>Довідка</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="752"/>
+ <source>About</source>
+ <translation>Про програму</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1203"/>
+ <source>The study cannot be started.</source>
+ <comment>First part of error message</comment>
+ <translation>Неможливо почати навчання.</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="692"/>
+ <source>Ctrl+I</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="671"/>
+ <source>&amp;Find...</source>
+ <translation>З&amp;найти...</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="672"/>
+ <source>Ctrl+F</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="675"/>
+ <source>Find &amp;again</source>
+ <translation>Знайти зн&amp;ову</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="709"/>
+ <source>&amp;Word drill</source>
+ <translation>&amp;Перегляд слів</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="170"/>
+ <source>Dictionaries</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Словники</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="480"/>
+ <source>Add image</source>
+ <translation>Вставити малюнок</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="482"/>
+ <source>Images</source>
+ <comment>Filter name in dialog</comment>
+ <translation>Малюнки</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="484"/>
+ <source>All files</source>
+ <translation>Всі файли</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="716"/>
+ <source>&amp;Spaced repetition</source>
+ <translation>&amp;Інтервальне повторення</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="731"/>
+ <source>&amp;Dictionary options</source>
+ <translation>Налаштування &amp;словника</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="741"/>
+ <source>&amp;Study settings</source>
+ <translation>Налаштування &amp;навчання</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="835"/>
+ <source>&amp;File</source>
+ <translation>&amp;Файл</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="854"/>
+ <source>&amp;Recent files</source>
+ <translation>&amp;Останні файли</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="800"/>
+ <source>&amp;Edit</source>
+ <translation>&amp;Правлення</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="617"/>
+ <source>Online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="649"/>
+ <source>Ctrl+Z</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="652"/>
+ <source>Ctrl+Y</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="682"/>
+ <source>&amp;Add image</source>
+ <translation>Вставити &amp;малюнок</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="683"/>
+ <source>Ctrl+G</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="691"/>
+ <source>&amp;Insert record</source>
+ <translation>Вставити &amp;запис</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="699"/>
+ <source>&amp;Remove record</source>
+ <translation>Ви&amp;далити запис</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="817"/>
+ <source>&amp;View</source>
+ <translation>&amp;Вигляд</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="819"/>
+ <source>&amp;Tools</source>
+ <translation>&amp;Інструменти</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="865"/>
+ <source>&amp;Options</source>
+ <translation>Пара&amp;метри</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="828"/>
+ <source>&amp;Help</source>
+ <translation>&amp;Довідка</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="875"/>
+ <source>Main</source>
+ <translation>Головна</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="947"/>
+ <source>Card packs</source>
+ <translation>Колоди карток</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/MainWindow.cpp" line="1103"/>
+ <source>The pasted records contain %n new field(s)</source>
+ <translation>
+ <numerusform>Вставлені записи містять %n нове поле</numerusform>
+ <numerusform>Вставлені записи містять %n нових поля</numerusform>
+ <numerusform>Вставлені записи містять %n нових полей</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1104"/>
+ <source>Do you want to add new fields to this dictionary?</source>
+ <translation>Ви хочете додати нові поля до цього словника?</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1106"/>
+ <source>Add new fields</source>
+ <translation>Додати нові поля</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/MainWindow.cpp" line="1107"/>
+ <source>Paste only existing fields</source>
+ <translation>Вставити тільки існуючі поля</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="81"/>
+ <source>Website:</source>
+ <translation>Веб-сайт:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="87"/>
+ <source>Usage:</source>
+ <translation>Використання:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="88"/>
+ <source>FILE is a dictionary filename to load.</source>
+ <translation>FILE - ім’я файла словника, що завантажується.</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="89"/>
+ <source>Options:</source>
+ <translation>Параметри:</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="90"/>
+ <source>Display this help and exit</source>
+ <translation>Показати цю довідку і вийти</translation>
+ </message>
+ <message>
+ <location filename="../src/main.cpp" line="91"/>
+ <source>Output version information and exit</source>
+ <translation>Вивести інформацію про версію і вийти</translation>
+ </message>
+</context>
+<context>
+ <name>PacksPage</name>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="49"/>
+ <source>Add</source>
+ <translation>Додати</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="50"/>
+ <source>Remove</source>
+ <translation>Видалити</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="61"/>
+ <source>Move pack up</source>
+ <translation>Пересунути колоду вгору</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="65"/>
+ <source>Move pack down</source>
+ <translation>Пересунути колоду вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="75"/>
+ <source>Card packs</source>
+ <translation>Колоди карток</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="104"/>
+ <source>Move field up</source>
+ <translation>Пересунути поле вгору</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="108"/>
+ <source>Move field down</source>
+ <translation>Пересунути поле вниз</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="118"/>
+ <source>Pack fields</source>
+ <translation>Поля колоди</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="152"/>
+ <source>Remove field from pack</source>
+ <translation>Видалити поле з колоди</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="155"/>
+ <source>Add field to pack</source>
+ <translation>Додати поле до колоди</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="163"/>
+ <source>Uses exact answer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="169"/>
+ <source>Unused fields</source>
+ <translation>Невикористані поля</translation>
+ </message>
+ <message>
+ <location filename="../src/dic-options/PacksPage.cpp" line="181"/>
+ <source>Preview</source>
+ <translation>Перегляд</translation>
+ </message>
+</context>
+<context>
+ <name>PacksTreeModel</name>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="35"/>
+ <source>Card pack</source>
+ <translation>Колода карток</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="36"/>
+ <source>Sched</source>
+ <translation>Заплан</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/PacksTreeModel.cpp" line="37"/>
+ <source>New</source>
+ <translation>Нов</translation>
+ </message>
+</context>
+<context>
+ <name>ProgressPage</name>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="37"/>
+ <source>Studied</source>
+ <translation type="unfinished">Вивчені</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="38"/>
+ <source>Scheduled for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="39"/>
+ <source>New</source>
+ <translation type="unfinished">Нов</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.cpp" line="22"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Всього: %1</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/ProgressPage.h" line="16"/>
+ <source>Study progress</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="153"/>
+ <source>Insert %n record(s)</source>
+ <comment>Undo action of inserting records</comment>
+ <translation>
+ <numerusform>Вставити %n запис</numerusform>
+ <numerusform>Вставити %n записи</numerusform>
+ <numerusform>Вставити %n записів</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="222"/>
+ <source>Remove %n record(s)</source>
+ <comment>Undo action of removing records</comment>
+ <translation>
+ <numerusform>Видалити %n запис</numerusform>
+ <numerusform>Видалити %n записи</numerusform>
+ <numerusform>Видалити %n записів</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/UndoCommands.cpp" line="257"/>
+ <source>Edit &quot;%1&quot;</source>
+ <comment>Undo action of editing a record</comment>
+ <translation>Редагувати &quot;%1&quot;</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/main-view/UndoCommands.cpp" line="303"/>
+ <source>Paste %n record(s)</source>
+ <comment>Undo action of pasting records</comment>
+ <translation>
+ <numerusform>Вставити з буфера %n запис</numerusform>
+ <numerusform>Вставити з буфера %n записи</numerusform>
+ <numerusform>Вставити з буфера %n записів</numerusform>
+ </translation>
+ </message>
+</context>
+<context>
+ <name>ScheduledPage</name>
+ <message>
+ <location filename="../src/statistics/ScheduledPage.h" line="11"/>
+ <source>Scheduled cards</source>
+ <translation>Заплановані картки</translation>
+ </message>
+</context>
+<context>
+ <name>SpacedRepetitionWindow</name>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="10"/>
+ <source>Spaced repetition</source>
+ <translation>Інтервальне повторення</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="64"/>
+ <source>Today learned new cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="68"/>
+ <source>Scheduled learning reviews:
+new cards must be repeated today to learn</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="73"/>
+ <source>Time left to the next learning review</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="76"/>
+ <source>Scheduled cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="80"/>
+ <source>New scheduled cards for today:
+new cards that will be shown between the scheduled ones</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="120"/>
+ <source>Reviewed cards</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="121"/>
+ <source>Learning reviews</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="122"/>
+ <source>Scheduled cards</source>
+ <translation type="unfinished">Заплановані картки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="123"/>
+ <source>New cards for today</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="127"/>
+ <source>Progress of reviews scheduled for today:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="136"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="137"/>
+ <source>Completely forgotten card, couldn&apos;t recall the answer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="138"/>
+ <source>Incorrect</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="139"/>
+ <source>The answer is incorrect.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="140"/>
+ <source>Difficult</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="141"/>
+ <source>It&apos;s difficult to recall the answer. The last interval was too long.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="142"/>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>Good</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="143"/>
+ <source>The answer is recalled in couple of seconds. The last interval was good enough.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="144"/>
+ <source>Easy</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="145"/>
+ <source>The card is too easy, and recalled without any effort. The last interval was too short.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="243"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="312"/>
+ <source>(%1 min)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="333"/>
+ <source>Day cards limit is reached: %1 cards.
+It is recommended to stop studying this dictionary.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="352"/>
+ <source>All cards are reviewed</source>
+ <translation>Усі картки переглянуті</translation>
+ </message>
+ <message>
+ <location filename="../src/study/SpacedRepetitionWindow.cpp" line="354"/>
+ <source>You can go to the next pack or dictionary, or open the Word drill.</source>
+ <translation>Ви можете перейти до наступної колоди або словнику, або відкрити Перегляд слів.</translation>
+ </message>
+</context>
+<context>
+ <name>StatisticsView</name>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="25"/>
+ <source>Statistics</source>
+ <translation>Статистика</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="106"/>
+ <source>Card pack:</source>
+ <translation>Колода карток:</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="109"/>
+ <source>Period:</source>
+ <translation>Період:</translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="142"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="143"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="144"/>
+ <source>%n week(s)</source>
+ <translation>
+ <numerusform>%n тиждень</numerusform>
+ <numerusform>%n тижня</numerusform>
+ <numerusform>%n тижнів</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="145"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="146"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="147"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="148"/>
+ <source>%n month(s)</source>
+ <translation>
+ <numerusform>%n місяць</numerusform>
+ <numerusform>%n місяця</numerusform>
+ <numerusform>%n місяців</numerusform>
+ </translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../src/statistics/StatisticsView.cpp" line="149"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="150"/>
+ <location filename="../src/statistics/StatisticsView.cpp" line="151"/>
+ <source>%n year(s)</source>
+ <translation>
+ <numerusform>%n рік</numerusform>
+ <numerusform>%n роки</numerusform>
+ <numerusform>%n років</numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/StatisticsView.cpp" line="152"/>
+ <source>All time</source>
+ <translation>Увесь час</translation>
+ </message>
+</context>
+<context>
+ <name>Strings</name>
+ <message>
+ <location filename="../src/strings.cpp" line="4"/>
+ <source>Author: Mykhaylo Kopytonenko</source>
+ <translation>Автор: Михайло Копитоненко</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="3"/>
+ <source>Build</source>
+ <translation>Збірка</translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="5"/>
+ <source>Fresh Memory</source>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/strings.cpp" line="6"/>
+ <source>Error</source>
+ <translation>Помилка</translation>
+ </message>
+</context>
+<context>
+ <name>StudiedPage</name>
+ <message>
+ <location filename="../src/statistics/StudiedPage.h" line="11"/>
+ <source>Studied cards</source>
+ <translation>Вивчені картки</translation>
+ </message>
+</context>
+<context>
+ <name>StudySettingsDialog</name>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="31"/>
+ <source>Add new cards in random order</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="72"/>
+ <source>Day starts at, o&apos;clock:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="73"/>
+ <source>Share of new cards:</source>
+ <translation>Частка нових карток:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="74"/>
+ <source>Repetition interval randomness:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="92"/>
+ <source>Day reviews limit:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="95"/>
+ <source>Don&apos;t add new cards after scheduled cards threshold:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="97"/>
+ <source>Limits</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="93"/>
+ <source>Day limit of new cards:</source>
+ <translation>Денне обмеження для нових карток:</translation>
+ </message>
+ <message>
+ <location filename="../src/settings/StudySettingsDialog.cpp" line="20"/>
+ <source>Study settings</source>
+ <translation>Налаштування навчання</translation>
+ </message>
+</context>
+<context>
+ <name>StylePreviewModel</name>
+ <message>
+ <location filename="../src/settings/StylePreviewModel.cpp" line="38"/>
+ <source>keyword</source>
+ <translation>ключове слово</translation>
+ </message>
+</context>
+<context>
+ <name>TimeChartPage</name>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Date</source>
+ <translation type="unfinished">Дата</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="14"/>
+ <source>Cards</source>
+ <translation type="unfinished">Картки</translation>
+ </message>
+ <message>
+ <location filename="../src/statistics/TimeChartPage.cpp" line="25"/>
+ <source>Total: %1</source>
+ <translation type="unfinished">Всього: %1</translation>
+ </message>
+</context>
+<context>
+ <name>WelcomeScreen</name>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="7"/>
+ <source>Create new dictionary</source>
+ <translation>Створити новий словник</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="9"/>
+ <source>Open existing dictionary</source>
+ <translation>Відкрити існуючий словник</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="11"/>
+ <source>Open online dictionaries</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="13"/>
+ <source>Import from CSV file</source>
+ <translation>Імпорт із CSV файла</translation>
+ </message>
+ <message>
+ <location filename="../src/main-view/WelcomeScreen.cpp" line="15"/>
+ <source>Recent dictionaries</source>
+ <translation>Останні словники</translation>
+ </message>
+</context>
+<context>
+ <name>WordDrillWindow</name>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="8"/>
+ <source>Word drill</source>
+ <translation>Перегляд слів</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="26"/>
+ <source>Current card / All cards</source>
+ <translation>Поточна картка / Усі картки</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="30"/>
+ <source>Progress of reviewing cards</source>
+ <translation>Станов перегляду карток</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="31"/>
+ <source>Show answers</source>
+ <translation>Показувати відповіді</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="32"/>
+ <source>S</source>
+ <comment>Shortcut for &apos;Show answers&apos; checkbox</comment>
+ <translation></translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="40"/>
+ <source>Back</source>
+ <translation>Назад</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="42"/>
+ <source>Go back in history</source>
+ <translation>Повернутися назад по історії</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="45"/>
+ <source>Forward</source>
+ <translation>Вперед</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="47"/>
+ <source>Go forward in history</source>
+ <translation>Пройти вперед по історії</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="50"/>
+ <source>Next</source>
+ <translation>Далі</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="52"/>
+ <source>Show next card (Enter)</source>
+ <translation>Показати наступну картку (Enter)</translation>
+ </message>
+ <message>
+ <location filename="../src/study/WordDrillWindow.cpp" line="105"/>
+ <source>No cards available</source>
+ <translation>Немає карток</translation>
+ </message>
+</context>
+</TS>
diff --git a/userdocs/_static/default.css b/userdocs/_static/default.css
new file mode 100644
index 0000000..e997e53
--- /dev/null
+++ b/userdocs/_static/default.css
@@ -0,0 +1,498 @@
+body {
+ font: 12pt 'Verdana', sans-serif;
+ min-width: 740px;
+ text-align: justify;
+}
+
+#contents {width: initial; padding: 10px; text-align: justify}
+
+#right {margin: 0 0 0 20px;}
+
+em.menuselection {
+ background-color: #f3f3f3;
+ padding: 2px 3px;
+ font-style: normal;
+ font-size: inherit;}
+
+em.guilabel {
+ background-color: #f3f3f3;
+ padding: 2px 3px;
+ }
+
+tt.kbd {
+ font-size: inherit;
+ font-family: inherit;
+ display: inline-block;
+ padding: 0px 2px;
+ font-weight: normal;
+ border: 1px solid black;
+ border-radius: 5px;}
+
+.hint {display: block; margin-left: 20px; min-height: 5ex}
+.note {display: block; margin-left: 20px; min-height: 5ex}
+
+.since {font-weight: bold;
+ font-size: 80%;}
+
+.versionmodified {font-style: italic;
+ font-size: 90%;}
+
+p img, img.icon, td img {height: 20px; vertical-align: text-bottom}
+.figure {
+ text-align: center;
+ margin: 2em 2em;
+ }
+.figure p.caption {font-weight: bold}
+
+table {margin: 2em 2em;}
+table caption {
+ font-weight: bold;
+ margin: 1em 1em;}
+
+
+pre {
+ font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.95em;
+ letter-spacing: 0.015em;
+ padding: 0.5em;
+ border: 1px solid #ccc;
+ background-color: #f8f8f8;
+}
+
+td.linenos pre {
+ padding: 0.5em 0;
+ border: 0;
+ background-color: transparent;
+ color: #aaa;
+}
+
+table.highlighttable {
+ margin-left: 0.5em;
+}
+
+table.highlighttable td {
+ padding: 0 0.5em 0 0.5em;
+}
+
+cite, code, tt {
+ font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+ font-size: 0.95em;
+ letter-spacing: 0.01em;
+}
+
+tt {
+ background-color: #f2f2f2;
+ border-bottom: 1px solid #ddd;
+ color: #333;
+}
+
+tt.descname {
+ background-color: transparent;
+ font-weight: bold;
+ font-size: 1.2em;
+ border: 0;
+}
+
+tt.descclassname {
+ background-color: transparent;
+ border: 0;
+}
+
+tt.xref {
+ background-color: transparent;
+ font-weight: bold;
+ border: 0;
+}
+
+a tt {
+ background-color: transparent;
+ font-weight: bold;
+ border: 0;
+ color: #CA7900;
+}
+
+a tt:hover {
+ color: #2491CF;
+}
+
+dl {
+ margin-bottom: 15px;
+}
+
+dd p {
+ margin-top: 0px;
+}
+
+dd ul, dd table {
+ margin-bottom: 10px;
+}
+
+dd {
+ margin-top: 3px;
+ margin-bottom: 10px;
+ margin-left: 30px;
+}
+
+.refcount {
+ color: #060;
+}
+
+dt:target,
+.highlight {
+ background-color: #fbe54e;
+}
+
+dl.class, dl.function {
+ border-top: 2px solid #888;
+}
+
+dl.method, dl.attribute {
+ border-top: 1px solid #aaa;
+}
+
+dl.glossary dt {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+pre {
+ line-height: 120%;
+}
+
+pre a {
+ color: inherit;
+ text-decoration: underline;
+}
+
+.first {
+ margin-top: 0 !important;
+}
+
+div.clearer {
+ clear: both;
+}
+
+div.related h3 {
+ display: none;
+}
+
+div.related ul {
+ background-image: url(navigation.png);
+ height: 2em;
+ list-style: none;
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 0;
+ padding-left: 10px;
+}
+
+div.related ul li {
+ margin: 0;
+ padding: 0;
+ height: 2em;
+ float: left;
+}
+
+div.related ul li.right {
+ float: right;
+ margin-right: 5px;
+}
+
+div.related ul li a {
+ margin: 0;
+ padding: 0 5px 0 5px;
+ line-height: 1.75em;
+}
+
+div.body {
+ margin: 0;
+ padding: 0.5em 20px 20px 20px;
+}
+
+div.bodywrapper {
+ margin: 0 240px 0 0;
+ border-right: 1px solid #ccc;
+}
+
+div.sphinxsidebar {
+ margin: 0;
+ padding: 0.5em 15px 15px 0;
+ width: 210px;
+ float: right;
+ text-align: left;
+}
+
+div.sphinxsidebar h4, div.sphinxsidebar h3 {
+ margin: 1em 0 0.5em 0;
+ font-size: 0.9em;
+ padding: 0.1em 0 0.1em 0.5em;
+ color: white;
+ border: 1px solid #86989B;
+ background-color: #AFC1C4;
+}
+
+div.sphinxsidebar ul {
+ padding-left: 1.5em;
+ margin-top: 7px;
+ list-style: none;
+ padding: 0;
+ line-height: 130%;
+}
+
+div.sphinxsidebar ul ul {
+ list-style: square;
+ margin-left: 20px;
+}
+
+p {
+ margin: 0.8em 0 0.5em 0;
+}
+
+p.rubric {
+ font-weight: bold;
+}
+
+h1 {
+ margin: 0;
+ padding: 0.7em 0 0.3em 0;
+}
+
+h2 {
+ margin: 1.3em 0 0.2em 0;
+ font-size: 1.35em;
+ padding: 0;
+}
+
+h3 {
+ margin: 1em 0 -0.3em 0;
+ font-size: 1.2em;
+}
+
+h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
+ color: black!important;
+}
+
+h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
+ display: none;
+ margin: 0 0 0 0.3em;
+ padding: 0 0.2em 0 0.2em;
+ color: #aaa!important;
+}
+
+h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
+h5:hover a.anchor, h6:hover a.anchor {
+ display: inline;
+}
+
+h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
+h5 a.anchor:hover, h6 a.anchor:hover {
+ color: #777;
+ background-color: #eee;
+}
+
+table {
+ border-collapse: collapse;
+ margin: 0 -0.5em 0 -0.5em;
+}
+
+table td, table th {
+ padding: 0.2em 0.5em 0.2em 0.5em;
+}
+
+div.footer {
+ background-color: #E3EFF1;
+ color: #86989B;
+ padding: 3px 8px 3px 0;
+ clear: both;
+ font-size: 0.8em;
+ text-align: right;
+}
+
+div.footer a {
+ color: #86989B;
+ text-decoration: underline;
+}
+
+div.pagination {
+ margin-top: 2em;
+ padding-top: 0.5em;
+ border-top: 1px solid black;
+ text-align: center;
+}
+
+div.sphinxsidebar ul.toc {
+ margin: 1em 0 1em 0;
+ padding: 0 0 0 0.5em;
+ list-style: none;
+}
+
+div.sphinxsidebar ul.toc li {
+ margin: 0.5em 0 0.5em 0;
+ font-size: 0.9em;
+ line-height: 130%;
+}
+
+div.sphinxsidebar ul.toc li p {
+ margin: 0;
+ padding: 0;
+}
+
+div.sphinxsidebar ul.toc ul {
+ margin: 0.2em 0 0.2em 0;
+ padding: 0 0 0 1.8em;
+}
+
+div.sphinxsidebar ul.toc ul li {
+ padding: 0;
+}
+
+div.admonition, div.warning {
+ margin: 2em auto 1em 2em;
+ width: 70%;
+ border: 1px solid #86989B;
+ background-color: #f7f7f7;
+}
+
+div.admonition p, div.warning p {
+ margin: 0.5em 1em 0.5em 1em;
+ padding: 0;
+}
+
+div.admonition pre, div.warning pre {
+ margin: 0.4em 1em 0.4em 1em;
+}
+
+div.admonition p.admonition-title,
+div.warning p.admonition-title {
+ margin: 0;
+ padding: 0.1em 0 0.1em 0.5em;
+ background-color: #242475;
+ color: white;
+ font-weight: bold;
+}
+
+div.warning {
+ border: 1px solid #940000;
+}
+
+div.warning p.admonition-title {
+ background-color: #CF0000;
+ border-bottom-color: #940000;
+}
+
+div.admonition ul, div.admonition ol,
+div.warning ul, div.warning ol {
+ margin: 0.1em 0.5em 0.5em 3em;
+ padding: 0;
+}
+
+div.versioninfo {
+ margin: 1em 0 0 0;
+ border: 1px solid #ccc;
+ background-color: #DDEAF0;
+ padding: 8px;
+ line-height: 1.3em;
+ font-size: 0.9em;
+}
+
+
+a.headerlink {
+ color: #gray !important;
+ font-size: 1em;
+ margin-left: 6px;
+ padding: 0 4px 0 4px;
+ text-decoration: none!important;
+ visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink {
+ visibility: visible;
+}
+
+a.headerlink:hover {
+ background-color: #ccc;
+ color: white!important;
+}
+
+table.indextable td {
+ text-align: left;
+ vertical-align: top;
+}
+
+table.indextable dl, table.indextable dd {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+table.indextable tr.pcap {
+ height: 10px;
+}
+
+table.indextable tr.cap {
+ margin-top: 10px;
+ background-color: #f2f2f2;
+}
+
+img.toggler {
+ margin-right: 3px;
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+img.inheritance {
+ border: 0px
+}
+
+form.pfform {
+ margin: 10px 0 20px 0;
+}
+
+table.contentstable {
+ width: 90%;
+}
+
+table.contentstable p.biglink {
+ line-height: 150%;
+}
+
+a.biglink {
+ font-size: 1.3em;
+}
+
+span.linkdescr {
+ font-style: italic;
+ padding-top: 5px;
+ font-size: 90%;
+}
+
+ul.search {
+ margin: 10px 0 0 20px;
+ padding: 0;
+}
+
+ul.search li {
+ padding: 5px 0 5px 20px;
+ background-image: url(file.png);
+ background-repeat: no-repeat;
+ background-position: 0 7px;
+}
+
+ul.search li a {
+ font-weight: bold;
+}
+
+ul.search li div.context {
+ color: #888;
+ margin: 2px 0 0 30px;
+ text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+ font-weight: bold;
+}
diff --git a/userdocs/_static/icon-note.png b/userdocs/_static/icon-note.png
new file mode 100644
index 0000000..da46876
--- /dev/null
+++ b/userdocs/_static/icon-note.png
Binary files differ
diff --git a/userdocs/_static/icon-tip.png b/userdocs/_static/icon-tip.png
new file mode 100644
index 0000000..1c68f29
--- /dev/null
+++ b/userdocs/_static/icon-tip.png
Binary files differ
diff --git a/userdocs/_templates/layout.html b/userdocs/_templates/layout.html
new file mode 100644
index 0000000..a82dc76
--- /dev/null
+++ b/userdocs/_templates/layout.html
@@ -0,0 +1,10 @@
+{% extends "!layout.html" %}
+
+
+{% block rootrellink %}
+ <li><a href="{{ pathto('index') }}">home</a></li>
+{% endblock %}
+
+{% block sidebar1 %}{{ sidebar() }}{% endblock %}
+{% block sidebar2 %}{% endblock %}
+
diff --git a/userdocs/_templates/version.html b/userdocs/_templates/version.html
new file mode 100644
index 0000000..beba79c
--- /dev/null
+++ b/userdocs/_templates/version.html
@@ -0,0 +1,2 @@
+<div id="version"><p>Version {{ version }}<p></div>
+
diff --git a/userdocs/about.md b/userdocs/about.md
new file mode 100644
index 0000000..323145b
--- /dev/null
+++ b/userdocs/about.md
@@ -0,0 +1,28 @@
+Fresh Memory is an education application for studying languages with [Spaced Repetition][spacedrep] method. Its primary purpose is to learn and repeat foreign words. But other areas can be studied as well, for example, country's capitals, flags, mathematical formulas, and even friends' birthdays. The study material is stored as collections of _flash cards_.
+
+Fresh Memory has two studying modes: _Word drill_ and _Spaced repetition_. The Word drill is the classic random browsing of flash cards. It can be used for getting familiar with the cards. The Spaced repetition is the main tool for repeating the cards. It automatically schedules repetition intervals of each card according to its difficulty. This method allows to quickly study any structured material and keep it in memory for a long time.
+
+In **Spaced repetition**, the user first sees a card question. He thinks of the answer, and opens the correct answer. The user checks if his answer was correct and evaluates himself with grades from 0 to 5. The grades depend on how difficult it was to recall the card. The application then schedules the next repetition of the card depending on the grade. With each repetition, the next interval automatically increases, as the user knows the card better. The repetition intervals may range from several minutes to over a year, without a limit. The easier the card is for the user, the longer its interval becomes. If a card is difficult to remember, it is shown more often so that the user would have more chance to study it.
+
+The flash cards are stored in _dictionaries_. The user himself can create the dictionaries and add cards to them. The application allows to open several dictionaries in tabs at the same time. The user can see basic study statistics of each dictionary: number of studied and new cards, scheduled cards for today. Studying is performed with one selected dictionary.
+
+**Multi-sided cards**. Paper flash cards have two sides: question and answer. Computer cards can have multiple fields. These fields are needed to store auxiliary information: word pronunciation, example, etc. The card fields can be configured which to show for the question, and for the answer. These field configurations are called _card packs_, because they emulate a collection of ready cards compiled from the dictionary.
+
+The main window has cards editor, which allows adding and editing cards easily. The user can freely choose where to store the dictionary files in his computer. The card fields can be formatted with different font types, sizes and colors. It is possible to set the background color for cards. Cards can have images.
+
+It is possible to study words in two directions: from foreign to native language and in the reversed order. This is achieved by configuring needed card packs. The study directions can be more than two. Consider a dictionary, which has Country, Flag and Capital fields. The user can study cards in the following configurations: Country - Flag, Flag - Country, Country - Capital and Capital - Country.
+
+During repetition, the user can edit or remove the current card right in the Study view. It is not required to switch to the Dictionary view and search the needed card for editing.
+
+There can be cards, which have the same question, but different answers (_homonyms_). When repeating such cards, the user would have nothing to do but merely guess which answer was meant for the question. Fresh Memory automatically merges cards with the same question into one. The resulting card has the common question and the answers from all original cards. And in the contrary, there can be a card with a complex question containing several words, which all correspond to the same answer (_synonyms_). Fresh Memory automatically breaks down cards with complex questions into several cards.
+
+The dictionary files use custom XML format (with extension .fmd). Fresh Memory allows to export and import the flash cards to/from [CSV][csv] files (in plain text). Exporting to CSV allows to edit the cards in other programs. Importing from CSV format allows to use ready-made cards, which were created in another program or just a text editor.
+
+The repeated cards and their repetition information are stored in _study files_. A study file is stored beside its dictionary file in the same directory - it has the same name, but different extension (.fms). The dictionary file always stays clean from any user-specific data, thus it is ready to be shared with other users at any time. As the dictionaries and study files are in the same directory, it is easy to copy them together.
+
+**Portability**. It is possible to continue working with your cards and dictionaries on different computers and even operating systems. For that purpose, the dictionary and study files must be carried over to another computer with a USB disk or another method. It is even possible to keep your cards in a USB disk or a network shared drive, and use them directly without copying between computers.
+
+
+[spacedrep]: http://en.wikipedia.org/wiki/Spaced_repetition
+[csv]: http://en.wikipedia.org/wiki/Comma-separated_values
+
diff --git a/userdocs/activation.rst b/userdocs/activation.rst
new file mode 100644
index 0000000..9501a5c
--- /dev/null
+++ b/userdocs/activation.rst
@@ -0,0 +1,92 @@
+Activation
+==========
+
+Fresh Memory must be activated after the installation. The activation requires a *Product Key*, which is sent by email after purchasing the application.
+
+One purchased license allows activation on 3 computers. However, it is limited for personal usage of one user or his/her family. Friends, co-workers etc. must purchase their own licenses. Additional licenses for the same user can be bought with a 20% discount. Upgrade to the next version gives discount of 2 €. Find the *Discount code* in the purchase confirmation email.
+
+The license is permanent: after it is purchased, the application can be used forever. The application can be re-activated on the same computer many times (without using another free activation). Thus, it is possible to re-activate Fresh Memory after re-installation of the operating system.
+
+The application offers a *trial period* for 15 days. During this period, Fresh Memory can be used free of charge and with its full functionality. After the trial period expires, the application must be purchased to continue using it.
+
+* One license is for 3 computers of one user
+* Additional licenses: -40%
+* Upgrade to the next version: -2 €
+* Trial period: 15 days
+
+Activating with product key
+---------------------------
+
+On the first launch, Fresh Memory will show the *Activation dialog*. Enter your Product key for activation (it can be copied directly from the email).
+
+.. figure:: images/activation/activation_dialog.png
+
+ Activation dialog
+
+Click :guilabel:`Activate` button to activate. If you want to try out the application before purchase, click :guilabel:`Free trial` button. In order to buy a license, click :guilabel:`Buy now` button, which will open the purchase page at Fresh Memory web site.
+
+During the activation, Fresh Memory needs an Internet connection.
+
+.. versionchanged:: 1.4.0 Offline activation (without Internet) is no longer supported.
+
+The application sends the product key and the computer hardware information to the Fresh Memory server, and receives the *License file*, which is saved on the computer. The license file is created for the specific computer and contains information about the activated hardware. On each start, the application checks if its license matches the computer it is running on.
+
+The license request and response are always encrypted to protect the user data. The received license file is kept at the computer in an encrypted form too. The license is decrypted only inside the application, during the checking process.
+
+If the product key is correct, the activation succeeds. The dialog shows that the application is now activated:
+
+.. figure:: images/activation/activation_dialog_activated.png
+
+ The application is successfully activated
+
+If the application cannot be activated on this computer, the dialog shows an error message.
+
+Successfully activated application shows the license information at the bottom of About dialog: :menuselection:`Help --> About`. The user full name and the activation date are shown.
+
+.. figure:: images/activation/about_activated.png
+
+ About dialog shows the license information
+
+Pressing :guilabel:`License` button opens a web page with the license text.
+
+Trial mode
+----------
+
+To start the trial period, click :guilabel:`Free trial` button in the Activation dialog. It will open a web page with a product key for the trial. Enter the displayed key to the dialog and press :guilabel:`Activate`. The dialog will show the activated trial mode. It is still possible to activate with the product key in this mode, see below.
+
+.. figure:: images/activation/activation_dialog_trial.png
+
+ The application is activated in trial mode
+
+In trial mode, the trial beginning and expiration dates are shown at the bottom of About dialog: :menuselection:`Help --> About`. From here, it is possible to activate with the product key or to buy a license. :guilabel:`Enter product key` button opens the Activation dialog for the product key. And :guilabel:`Buy now` button opens the purchase page in the web browser.
+
+.. figure:: images/activation/about_trial.png
+
+ About dialog shows the trial mode information
+
+Fresh Memory will work in the trial mode 15 days and allow to use its full functionality without any restriction. After this period expires, the application will again ask to activate with the Activation dialog. The user can buy a license, and continue using the application. Re-starting the trial after it expires is not be possible at the same computer. The trial mode can be enabled on different computers independently of each other. Try this application on as many computers as you like.
+
+When the trial period expires, the application will show the Activation dialog with message "Your trial period is expired". At this dialog, it is possible to activate the application or to buy a license.
+
+In the trial mode, the application will sometimes show a reminder about expiration of the trial period, see figure below. This is just a reminder, you can continue using Fresh Memory normally.
+
+.. figure:: images/activation/trial_reminder.png
+
+ Reminder about trial period
+
+
+License backup
+--------------
+
+It is possible to backup the license. It can be useful for quick re-activation of the program after, for example, re-installation of the operating system. This doesn't require Internet connection. However, the usual online re-activation with entering the same Product key is always possible.
+
+Find the license file "license.bin" at this path and save to a safe place:
+
+* Windows XP: ``C:\Documents and Settings\<User>\Application data\freshmemory``
+* Windows 7: ``C:\Users\<User>\AppData\Roaming\freshmemory``
+* Linux: ``~/.config/freshmemory``
+
+Later you can restore the license by copying it back to the same directory.
+
+Remember that the restored license will work only on the same computer.
+
diff --git a/userdocs/browsing-cards.rst b/userdocs/browsing-cards.rst
new file mode 100644
index 0000000..b747b25
--- /dev/null
+++ b/userdocs/browsing-cards.rst
@@ -0,0 +1,65 @@
+Browsing cards
+==============
+
+Card preview
+------------
+
+.. versionadded:: 1.2.0
+
+The main Dictionary view, where the records are edited, doesn't give enough impression how the cards would look in the study mode. Next to the main view, there is :guilabel:`Card preview` pane (at the left side by default). It shows the final look of the selected record.
+
+.. figure:: images/browsing.png
+
+ Browsing a card with Card preview
+
+The preview takes into account the current *card pack*, selected at the :guilabel:`Card packs` pane (above the Preview). In the example, the current pack is "English - Chinese, Pinyin", therefore the corresponsing fields are displayed in the preview in the specified order. Selecting another card pack will show how the card looks for that pack.
+
+Here is another example, where the Preview pane clearly shows the selected card with correct fonts and colors, defined by the field styles:
+
+.. figure:: images/browsing2.png
+
+ Card Preview shows field styles
+
+
+Word drill
+----------
+
+In order to look all cards one-by-one in randmon order, Fresh Memory has *Word drill* mode. It shows all cards of the current *card pack* in a separate window exactly as they look during study. You can use this mode to preview all available cards and get familiar with them. Think about it as the classic random browsing of cards.
+
+Start browsing with :menuselection:`Tools --> Word drill` |Word drill icon| (shortcut :kbd:`F5`). This will open a study window shown below.
+
+.. |Word drill icon| image:: ../images/word-drill.png
+
+.. figure:: images/word_drill.png
+
+ Word drill
+
+The top of the study view has the card pack name ("English - Russian"). It describes what fields are used in this card pack.
+
+The upper part of the card is the question, the lower is the answer. Pressing :guilabel:`Next` button shows the next card (shortcut :kbd:`Space` or :kbd:`Enter`). The cards are shown in random order.
+
+.. note::
+ The random order of shown cards can be switched off in :menuselection:`Options --> Study settings`. Uncheck the :guilabel:`Show cards randomly` checkbox.
+
+The number of the current card and the total number of all cards in the dictionary is shown under the answer. These numbers are illustrated also graphically with the progress bar.
+
+It is possible to return to the previous card by pressing the :guilabel:`Back` button (shortcut :kbd:`←`). You can return back in the history as many cards as you like. To go forward in the history, press the :guilabel:`Forward` button (shortcut :kbd:`→`). In the history mode, the number of the current card is shown as normal, and the number of the last seen card is shown in parenthesis (), see figure below.
+
+.. figure:: images/ss-word_drill_history.png
+
+ Browsing history
+
+Pressing the :guilabel:`Next` button always shows the next new card, and the history mode is canceled.
+
+When all cards in the dictionary are shown, the same cards will go on to the second round. This will be indicated with the |Cycle icon| icon and number of the cycle.
+
+.. |Cycle icon| image:: icons/passes.png
+
+.. figure:: images/ss-word_drill_second_cycle.png
+
+ Browsing in the second cycle
+
+By default, both the question and the answer are shown. When the :guilabel:`Show answers` checkbox is unchecked (shortcut :kbd:`S`), only the question part is shown first. The user must press the :guilabel:`Show answer` button (:kbd:`A`) to open the answer.
+
+The *Word drill* mode doesn't change the user's study history. The user can browse cards with Word drill, and it will not affect the study with *Spaced repetition*.
+ \ No newline at end of file
diff --git a/userdocs/cards-generation.rst b/userdocs/cards-generation.rst
new file mode 100644
index 0000000..bf498f3
--- /dev/null
+++ b/userdocs/cards-generation.rst
@@ -0,0 +1,30 @@
+.. _cards-generation:
+
+Cards generation
+=====================
+
+Dictionary records and card packs
+-----------------------------------
+
+Traditional paper flashcards have two sides, which are used for the question and the answer. When browsing cards, it is possible to choose which side is the question, and which is the answer. It allows to study "English-French" card pack from English to French and in the reverse direction. Though, if you want to add other information, like example or pronunciation, it must be written only on one card side. It is impossible to have a "third" side on a paper card and choose whether to use it in the question or in the answer.
+
+In Fresh Memory, two card sides correspond to a *dictionary record* having two *fields*. But unlike paper flashcards, computer cards can have multiple fields. These fields can be used to store additonal information (pronunciation, example, word in a third language). The dictionary is configured what fields are used for the question and what for the answer. There can be more than one such configuration, and they are called *card packs*. They emulate different collections of cards with the same material.
+
+With card packs, it is possible to study words in two directions: from foreign to native language and in the reversed order. The same dictionary records can be used with different card packs. Let us consider an example. The user has a dictionary with "English", "French" and "Example" fields. One card pack defines that the English word is the question, and French and Example are used in the answer. The second pack defines that the French is the question, and English and Example are the answer. When the user starts studying, the application automatically generates cards from the records and selected card pack. See figure below.
+
+.. figure:: images/cards_generation.png
+ :alt: Cards generation
+
+ Cards generation
+
+
+The study directions can be more than two. Consider a dictionary, which has Country, Flag and Capital fields. The user can study cards in the following configurations: Country - Flag, Flag - Country, Country - Capital and Capital - Country.
+
+
+Automatic merging and break-down of cards
+--------------------------------------------
+
+There can be cards, which have the same question, but different answers (*homonyms*). When seeing such cards, the user would have nothing to do but merely guess which answer was meant for the question. The application instead automatically merges cards with the same question into one. The resulting card has the common question and the answers from all original cards.
+
+And on the contrary, there can be a card with a complex question containing several words, which all correspond to the same answer (*synonyms*). Fresh Memory automatically breaks down cards with complex questions into several cards.
+
diff --git a/userdocs/conf.py b/userdocs/conf.py
new file mode 100644
index 0000000..c514599
--- /dev/null
+++ b/userdocs/conf.py
@@ -0,0 +1,259 @@
+# -*- coding: utf-8 -*-
+#
+# Fresh Memory documentation build configuration file, created by
+# sphinx-quickstart on Wed Oct 1 15:55:47 2014.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Fresh Memory'
+copyright = u'2015, Mykhaylo Kopytonenko'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '1.4'
+# The full version, including alpha/beta/rc tags.
+release = '1.4.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+html_sidebars = {
+ '**': ['version.html', 'localtoc.html', 'relations.html', 'sourcelink.html']}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+html_use_index = False
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = False
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+html_show_copyright = False
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'FreshMemorydoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ ('index', 'FreshMemory.tex', u'Fresh Memory Documentation',
+ u'Mykhaylo Kopytonenko', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'freshmemory', u'Fresh Memory Documentation',
+ [u'Mykhaylo Kopytonenko'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'FreshMemory', u'Fresh Memory Documentation',
+ u'Mykhaylo Kopytonenko', 'FreshMemory', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/userdocs/diagrams/cards_generation.dia b/userdocs/diagrams/cards_generation.dia
new file mode 100644
index 0000000..2b6de92
--- /dev/null
+++ b/userdocs/diagrams/cards_generation.dia
Binary files differ
diff --git a/userdocs/diagrams/records.png b/userdocs/diagrams/records.png
new file mode 100644
index 0000000..96e3add
--- /dev/null
+++ b/userdocs/diagrams/records.png
Binary files differ
diff --git a/userdocs/dictionary-and-study.rst b/userdocs/dictionary-and-study.rst
new file mode 100644
index 0000000..a168d28
--- /dev/null
+++ b/userdocs/dictionary-and-study.rst
@@ -0,0 +1,29 @@
+Dictionary and study files
+==========================
+
+Study files
+-----------
+
+The learned cards and their repetitions are stored in a special *study file*. It is kept in the same directory as the dictionary file. The study file has the dictionary name and "fms" extension. The dictionary itself always stays clean from any user-specific data, thus it is always ready to be shared with other people.
+
+.. versionadded:: 1.4.1
+
+The study data is automatically saved every 3 minutes, and also immediately when the study window is closed.
+
+.. note::
+ The study file must be always in the same directory as the dictionary. If the dictionary file is moved to another place without the study file, the application will not be able to find the study data, and will consider all cards as new.
+
+**Portability**. It is possible to continue working with your dictionaries on different computers and even operating systems. For that purpose, the dictionary and study files must be carried over to another computer. The dictionary files can be put to any place as long as its study file is copied together in the same directory.
+
+
+Export and import
+-----------------
+
+It is possible to export a dictionary to a CSV_ text file to further work with it in another program. As well it is easy to import ready-made dictionaries from CSV files (which were created in another program or just a text editor).
+
+.. _CSV: http://en.wikipedia.org/wiki/Comma-separated_values
+
+Export the dictionary with :menuselection:`File --> Export to CSV` command.
+
+Import a dictionary with :menuselection:`File --> Import from CSV` command.
+
diff --git a/userdocs/dictionary-options.rst b/userdocs/dictionary-options.rst
new file mode 100644
index 0000000..544b598
--- /dev/null
+++ b/userdocs/dictionary-options.rst
@@ -0,0 +1,90 @@
+Dictionary options
+==================
+
+.. _field-configuration:
+
+Field configuration
+-------------------
+
+The configuration of card fields can be edited in :menuselection:`Options --> Dictionary options`, tab :guilabel:`Fields`. See figure below.
+
+.. figure:: images/settings/field_options.png
+
+ Field configuration
+
+
+The list of fields is on the left side, and their preview with selected styles is on the right.
+
+It is possible to add new fields and remove existing ones with :guilabel:`Add` |Add icon| and :guilabel:`Remove` |Remove icon| buttons.
+In order to rename a field, select it and press :guilabel:`Rename` button (shortcut :kbd:`F2`) or double-click on the field name.
+
+.. |Add icon| image:: icons/add.png
+.. |Remove icon| image:: icons/delete.png
+
+To change the appearance of fields, change the field style. Double-click a :guilabel:`Style` cell or select a field and press :kbd:`Space`. Select a style from the drop-down list and press :kbd:`Enter`. The preview will update the appearance of the field. The styles can be edited in another dialog: :menuselection:`Options --> Fonts and color settings`, see :ref:`field-styles` subsection.
+
+The order of the fields can be changed with the arrow buttons |Up arrow icon| |Down arrow icon| to the right of the list. This affects only the order, in which the fields are shown in the dictionary view.
+
+.. |Up arrow icon| image:: icons/1uparrow.png
+.. |Down arrow icon| image:: icons/1downarrow.png
+
+
+Card pack configuration
+-----------------------
+
+The configuration of card packs can be edited in :menuselection:`Options --> Dictionary options`, tab :guilabel:`Card packs`. See figure below.
+
+.. figure:: images/settings/pack_options.png
+
+ Card pack configuration
+
+
+This dialog has three columns: Card packs, Pack fields and Preview. The :guilabel:`Card packs` column shows configured packs. The next column shows fields of the selected pack. And the last column shows preview of cards of the current pack, with the same fonts as will be seen during study.
+
+The :guilabel:`Pack fields` column has two parts: the top and bottom lists. The top list shows fields included to cards. The first field (highlighted) is the question field, the rest fields will be displayed in the answer. The order of the fields can be changed with the arrow buttons |Up arrow icon| |Down arrow icon| to the right of the list. The field items can be also dragged by mouse. The bottom list, :guilabel:`Unused fields`, has the fields, which are not included to cards. To move the fields between *used* and *unused*, press arrow buttons |Down move icon| |Up move icon| between the lists. The same can be done also with drag-and-drop.
+
+.. |Down move icon| image:: icons/down.png
+.. |Up move icon| image:: icons/up.png
+
+It is possible to add new packs or remove existing ones with :guilabel:`Add` |Add icon| and :guilabel:`Remove` |Remove icon| buttons. The packs are named automatically according to the contained fields.
+
+By default, a new dictionary has three fields: "Question", "Answer" and "Example". Two card packs are created. The first pack uses Question field as the question, and Answer and Example as the answer. The second pack uses the fields in reverse order: "Answer - Question, Example". It is easy to rename fields according to one's needs and set required card packs in the dictionary options.
+
+The fields can be renamed, added and removed in the neighboring :guilabel:`Fields` tab, see :ref:`field-configuration` subsection.
+
+It is possible to enable using the *exact answer* for the current pack. Check the checkbox :guilabel:`Uses exact answer`. If it is enabled, Spaced Repetition view will ask user's answer before showing the correct one.
+
+
+.. _field-styles:
+
+Field styles
+------------
+
+The appearance of the fields are controlled with *styles*. The field style defines its font, color and other parameters. One style can be re-used with many fields. The most often used style is "Normal", it is used by many fields by default.
+
+The configuration of styles can be edited in :menuselection:`Options --> Font and color settings`. See figure below.
+
+.. figure:: images/settings/field_style_options.png
+
+ Field styles
+
+This dialog has three parts: list of styles, style parameters and preview. The background color of cards can be changed at the top of the dialog. Click the colored rectangle to choose color.
+
+There are enough built-in styles for all purposes in practice. The "Normal" style is used by default with new fields. "Example" is for example fields, "Transcription" is for transcriptions. "Big" is for larger font, and two colored styles are for highlighted fields. The appearance of the styles can be, although, changed disregarding their names. The styles cannot be renamed, added or removed in this version.
+
+The style parameters are: font family, size, color, bold, italic, preffix, suffix, *keyword* style. The *keyword* is described below. The font is changed without any surprises, just click the corresponding controls. The preview will be updated on each change.
+
+The preffix is some text (usually one character) that is automatically inserted before the field content. The suffix is text inserted after the content. These parameters are rarely used. The use of preffix and suffix is illustrated with the "Transcription" style.
+
+Keyword style
+^^^^^^^^^^^^^
+
+In Fresh Memory, it is possible to highlight words of the question, if they appear in the answer. It is very convenient for the example field, when you want to highlight the word of the answer. The exact match of words from the answer are automatically highlighted. In other cases, when the question word is changed in the example, you can use manual highlighting by enclosing the word or phrase in brackets [] . For example, question "write" with example "Tom [wrote] a letter." will appear like in the figure below.
+
+.. figure:: images/settings/keyword.png
+
+ Keyword highlighting
+
+
+The highlighted words in the example field are called *keywords*. Their style can be edited too. In the style of the e.g. "Example" field check the :guilabel:`Keyword style` checkbox and select its color and font variant. All other parameters (font family and size) will be the same as the main style. The style preview shows keyword styles at the right side.
+
diff --git a/userdocs/icons/1downarrow.png b/userdocs/icons/1downarrow.png
new file mode 100644
index 0000000..ea9c00c
--- /dev/null
+++ b/userdocs/icons/1downarrow.png
Binary files differ
diff --git a/userdocs/icons/1uparrow.png b/userdocs/icons/1uparrow.png
new file mode 100644
index 0000000..d6c2b99
--- /dev/null
+++ b/userdocs/icons/1uparrow.png
Binary files differ
diff --git a/userdocs/icons/add.png b/userdocs/icons/add.png
new file mode 100644
index 0000000..0540a9b
--- /dev/null
+++ b/userdocs/icons/add.png
Binary files differ
diff --git a/userdocs/icons/delete.png b/userdocs/icons/delete.png
new file mode 100644
index 0000000..64089d7
--- /dev/null
+++ b/userdocs/icons/delete.png
Binary files differ
diff --git a/userdocs/icons/down.png b/userdocs/icons/down.png
new file mode 100644
index 0000000..cd92e2e
--- /dev/null
+++ b/userdocs/icons/down.png
Binary files differ
diff --git a/userdocs/icons/filenew.png b/userdocs/icons/filenew.png
new file mode 100644
index 0000000..6e838b3
--- /dev/null
+++ b/userdocs/icons/filenew.png
Binary files differ
diff --git a/userdocs/icons/filesave.png b/userdocs/icons/filesave.png
new file mode 100644
index 0000000..dd00abd
--- /dev/null
+++ b/userdocs/icons/filesave.png
Binary files differ
diff --git a/userdocs/icons/passes.png b/userdocs/icons/passes.png
new file mode 100644
index 0000000..586dfe6
--- /dev/null
+++ b/userdocs/icons/passes.png
Binary files differ
diff --git a/userdocs/icons/pencil.png b/userdocs/icons/pencil.png
new file mode 100644
index 0000000..82ed03a
--- /dev/null
+++ b/userdocs/icons/pencil.png
Binary files differ
diff --git a/userdocs/icons/red-cross.png b/userdocs/icons/red-cross.png
new file mode 100644
index 0000000..3abef06
--- /dev/null
+++ b/userdocs/icons/red-cross.png
Binary files differ
diff --git a/userdocs/icons/up.png b/userdocs/icons/up.png
new file mode 100644
index 0000000..a5b0944
--- /dev/null
+++ b/userdocs/icons/up.png
Binary files differ
diff --git a/userdocs/images/activation/about_activated.png b/userdocs/images/activation/about_activated.png
new file mode 100755
index 0000000..0d94f32
--- /dev/null
+++ b/userdocs/images/activation/about_activated.png
Binary files differ
diff --git a/userdocs/images/activation/about_trial.png b/userdocs/images/activation/about_trial.png
new file mode 100755
index 0000000..7069994
--- /dev/null
+++ b/userdocs/images/activation/about_trial.png
Binary files differ
diff --git a/userdocs/images/activation/activation_dialog.png b/userdocs/images/activation/activation_dialog.png
new file mode 100755
index 0000000..b986dc1
--- /dev/null
+++ b/userdocs/images/activation/activation_dialog.png
Binary files differ
diff --git a/userdocs/images/activation/activation_dialog_activated.png b/userdocs/images/activation/activation_dialog_activated.png
new file mode 100755
index 0000000..dc80e62
--- /dev/null
+++ b/userdocs/images/activation/activation_dialog_activated.png
Binary files differ
diff --git a/userdocs/images/activation/activation_dialog_trial.png b/userdocs/images/activation/activation_dialog_trial.png
new file mode 100755
index 0000000..76c9e27
--- /dev/null
+++ b/userdocs/images/activation/activation_dialog_trial.png
Binary files differ
diff --git a/userdocs/images/activation/trial_reminder.png b/userdocs/images/activation/trial_reminder.png
new file mode 100755
index 0000000..a5677d4
--- /dev/null
+++ b/userdocs/images/activation/trial_reminder.png
Binary files differ
diff --git a/userdocs/images/adding_image.png b/userdocs/images/adding_image.png
new file mode 100644
index 0000000..edea551
--- /dev/null
+++ b/userdocs/images/adding_image.png
Binary files differ
diff --git a/userdocs/images/browsing.png b/userdocs/images/browsing.png
new file mode 100644
index 0000000..98b560b
--- /dev/null
+++ b/userdocs/images/browsing.png
Binary files differ
diff --git a/userdocs/images/browsing2.png b/userdocs/images/browsing2.png
new file mode 100644
index 0000000..e905435
--- /dev/null
+++ b/userdocs/images/browsing2.png
Binary files differ
diff --git a/userdocs/images/cards_generation.png b/userdocs/images/cards_generation.png
new file mode 100644
index 0000000..3e94717
--- /dev/null
+++ b/userdocs/images/cards_generation.png
Binary files differ
diff --git a/userdocs/images/settings/field_options.png b/userdocs/images/settings/field_options.png
new file mode 100644
index 0000000..8424222
--- /dev/null
+++ b/userdocs/images/settings/field_options.png
Binary files differ
diff --git a/userdocs/images/settings/field_style_options.png b/userdocs/images/settings/field_style_options.png
new file mode 100644
index 0000000..997268e
--- /dev/null
+++ b/userdocs/images/settings/field_style_options.png
Binary files differ
diff --git a/userdocs/images/settings/keyword.png b/userdocs/images/settings/keyword.png
new file mode 100644
index 0000000..63dbd5e
--- /dev/null
+++ b/userdocs/images/settings/keyword.png
Binary files differ
diff --git a/userdocs/images/settings/pack_options.png b/userdocs/images/settings/pack_options.png
new file mode 100644
index 0000000..fa2c818
--- /dev/null
+++ b/userdocs/images/settings/pack_options.png
Binary files differ
diff --git a/userdocs/images/settings/study_settings.png b/userdocs/images/settings/study_settings.png
new file mode 100755
index 0000000..5eae221
--- /dev/null
+++ b/userdocs/images/settings/study_settings.png
Binary files differ
diff --git a/userdocs/images/spacedrep/edit_card.png b/userdocs/images/spacedrep/edit_card.png
new file mode 100644
index 0000000..1a2e70d
--- /dev/null
+++ b/userdocs/images/spacedrep/edit_card.png
Binary files differ
diff --git a/userdocs/images/spacedrep/new_card.png b/userdocs/images/spacedrep/new_card.png
new file mode 100644
index 0000000..6bc6953
--- /dev/null
+++ b/userdocs/images/spacedrep/new_card.png
Binary files differ
diff --git a/userdocs/images/spacedrep/progress_tooltip.png b/userdocs/images/spacedrep/progress_tooltip.png
new file mode 100755
index 0000000..51874d1
--- /dev/null
+++ b/userdocs/images/spacedrep/progress_tooltip.png
Binary files differ
diff --git a/userdocs/images/spacedrep/settings_exact_answer.png b/userdocs/images/spacedrep/settings_exact_answer.png
new file mode 100644
index 0000000..4ebd0e4
--- /dev/null
+++ b/userdocs/images/spacedrep/settings_exact_answer.png
Binary files differ
diff --git a/userdocs/images/spacedrep/spacedrep.png b/userdocs/images/spacedrep/spacedrep.png
new file mode 100644
index 0000000..32fdc80
--- /dev/null
+++ b/userdocs/images/spacedrep/spacedrep.png
Binary files differ
diff --git a/userdocs/images/spacedrep/spacedrep_exact_answer.png b/userdocs/images/spacedrep/spacedrep_exact_answer.png
new file mode 100644
index 0000000..4eb4999
--- /dev/null
+++ b/userdocs/images/spacedrep/spacedrep_exact_answer.png
Binary files differ
diff --git a/userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.png b/userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.png
new file mode 100644
index 0000000..b581a77
--- /dev/null
+++ b/userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.png
Binary files differ
diff --git a/userdocs/images/spacedrep/spacedrep_hidden_answer.png b/userdocs/images/spacedrep/spacedrep_hidden_answer.png
new file mode 100644
index 0000000..21aee6f
--- /dev/null
+++ b/userdocs/images/spacedrep/spacedrep_hidden_answer.png
Binary files differ
diff --git a/userdocs/images/spacedrep/study_progress.png b/userdocs/images/spacedrep/study_progress.png
new file mode 100644
index 0000000..58cc4aa
--- /dev/null
+++ b/userdocs/images/spacedrep/study_progress.png
Binary files differ
diff --git a/userdocs/images/ss-new_dictionary.png b/userdocs/images/ss-new_dictionary.png
new file mode 100644
index 0000000..3e2133a
--- /dev/null
+++ b/userdocs/images/ss-new_dictionary.png
Binary files differ
diff --git a/userdocs/images/ss-word_drill_back_enabled.png b/userdocs/images/ss-word_drill_back_enabled.png
new file mode 100644
index 0000000..e51217d
--- /dev/null
+++ b/userdocs/images/ss-word_drill_back_enabled.png
Binary files differ
diff --git a/userdocs/images/ss-word_drill_back_pressed.png b/userdocs/images/ss-word_drill_back_pressed.png
new file mode 100644
index 0000000..0522309
--- /dev/null
+++ b/userdocs/images/ss-word_drill_back_pressed.png
Binary files differ
diff --git a/userdocs/images/ss-word_drill_history.png b/userdocs/images/ss-word_drill_history.png
new file mode 100644
index 0000000..03b1d76
--- /dev/null
+++ b/userdocs/images/ss-word_drill_history.png
Binary files differ
diff --git a/userdocs/images/ss-word_drill_second_cycle.png b/userdocs/images/ss-word_drill_second_cycle.png
new file mode 100644
index 0000000..f5a3db8
--- /dev/null
+++ b/userdocs/images/ss-word_drill_second_cycle.png
Binary files differ
diff --git a/userdocs/images/stats/chart_tooltip.png b/userdocs/images/stats/chart_tooltip.png
new file mode 100644
index 0000000..89afaec
--- /dev/null
+++ b/userdocs/images/stats/chart_tooltip.png
Binary files differ
diff --git a/userdocs/images/stats/stats_progress.png b/userdocs/images/stats/stats_progress.png
new file mode 100644
index 0000000..3d5891e
--- /dev/null
+++ b/userdocs/images/stats/stats_progress.png
Binary files differ
diff --git a/userdocs/images/stats/stats_scheduled.png b/userdocs/images/stats/stats_scheduled.png
new file mode 100644
index 0000000..8d7bdd0
--- /dev/null
+++ b/userdocs/images/stats/stats_scheduled.png
Binary files differ
diff --git a/userdocs/images/stats/stats_studied.png b/userdocs/images/stats/stats_studied.png
new file mode 100644
index 0000000..ff5f362
--- /dev/null
+++ b/userdocs/images/stats/stats_studied.png
Binary files differ
diff --git a/userdocs/images/welcome_panel.png b/userdocs/images/welcome_panel.png
new file mode 100644
index 0000000..c93484a
--- /dev/null
+++ b/userdocs/images/welcome_panel.png
Binary files differ
diff --git a/userdocs/images/word_drill.png b/userdocs/images/word_drill.png
new file mode 100644
index 0000000..fb0e6b8
--- /dev/null
+++ b/userdocs/images/word_drill.png
Binary files differ
diff --git a/userdocs/index.rst b/userdocs/index.rst
new file mode 100644
index 0000000..e0ca840
--- /dev/null
+++ b/userdocs/index.rst
@@ -0,0 +1,19 @@
+Fresh Memory documentation
+==========================
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+ :numbered:
+
+ introduction
+ browsing-cards
+ studying-cards
+ cards-generation
+ statistics
+ dictionary-options
+ study-settings
+ dictionary-and-study
+ activation
+ \ No newline at end of file
diff --git a/userdocs/introduction.rst b/userdocs/introduction.rst
new file mode 100644
index 0000000..dda40b3
--- /dev/null
+++ b/userdocs/introduction.rst
@@ -0,0 +1,97 @@
+Introduction
+============
+
+Fresh Memory is an education application for studying languages with `Spaced Repetition`_ method. Its primary purpose is to learn and repeat foreign words. But other areas can be studied as well, for example, history, geography, medicine, mathematics. The study material is stored as collections of flashcards_.
+
+.. _Spaced Repetition: http://en.wikipedia.org/wiki/Spaced_repetition
+.. _flashcards: http://en.wikipedia.org/wiki/Flashcard
+
+
+Quick start
+===========
+
+.. versionadded:: 1.2.0
+
+When Fresh Memory is launched, it shows :guilabel:`Welcome panel` with buttons to create a new dictionary, open an existing one, open an example, etc. See the image below.
+
+.. figure:: images/welcome_panel.png
+
+ Welcome panel
+
+To quickly start and try out the application functionality, open an example. Click :guilabel:`Open examples` button (or in menu :menuselection:`File --> Open examples`) and select some file from the opened dialog. The examples are automatically installed with Fresh Memory.
+
+In order to create a new empty dictionary, click |New file icon| :guilabel:`Create new dictionary` button (or in menu :menuselection:`File --> New`, shortcut :kbd:`Ctrl+N`).
+
+If you want to import ready cards from another application, save them in CSV format. Then in Fresh Memory, click :guilabel:`Import from CSV file` (or in menu :menuselection:`File --> Import from CSV`).
+
+
+Creating cards
+==============
+
+The flashcards are made from *dictionaries*, which are text files. A dictionary consists of *dictionary records*. The records have the actual text and images, which will be displayed on flashcards.
+
+A record has several *fields*, and it is possible to define which fields are used in the ready flashcards. The application allows to define several combinations of fields to study the material from different points of view. Thus several sets of flashcards can be produced from the same dictionary. The set of flashcards is called a *card pack*. All flashcards are automatically generated when the study view is opened. See more about flashcards generation in :ref:`cards-generation` section.
+
+In order to start with cards, create a new dictionary selecting :menuselection:`File --> New` in the main menu (shortcut :kbd:`Ctrl + N` or :guilabel:`New` |New file icon| toolbar button). The new dictionary opens in the main window, shown below.
+
+.. figure:: images/ss-new_dictionary.png
+
+ New empty dictionary
+
+Each dictionary is opened in a separate tab. The new dictionary has default name "noname.fmd". The name can be changed when the dictionary is saved. The dictionary tab shows |Save file icon| icon and the asterisk (*) next to its name---this means that the dictionary has unsaved changes. In this case, a new dictionary was created, but not saved yet.
+
+The new dictionary has default fields "Question", "Answer" and "Example". They can be renamed in :menuselection:`Options --> Dictionary options`. For a simple example, these default names are sufficient. But it is recommended to give custom names to the fields, e.g. "English", "Russian" for English-Russian dictionary.
+
+Now you can add your cards. Double-click with mouse button on any dictionary field and start typing text. An alternative way to enter the editing mode is to select a record cell with mouse (or cursor keys) and start typing. When the text is entered, press :kbd:`Enter` key to commit the change---the editing focus will move to the next cell. If it was the last cell of the last row, a new row will be added.
+
+The typed text that was not confirmed yet with :kbd:`Enter` can be canceled with :kbd:`Esc` key. Any change can be undone with :menuselection:`Edit --> Undo` command (shortcut :kbd:`Ctrl + Z`). There is no limit how many changes can be undone. Undone changes can be re-done again with :menuselection:`Edit --> Redo` (:kbd:`Ctrl + Y`).
+
+Once the cards are added, save the dictionary with :menuselection:`File --> Save` (:kbd:`Ctrl + S` or :guilabel:`Save` |Save file icon| toolbar button). When saving for the first time, the application will ask for a dictionary name and location where to save it. Choose the path and name, and press :guilabel:`OK` button.
+
+The dictionary can be saved later in another place or under a different name using :menuselection:`File --> Save as`. The application will work with the new file, but the old one will be preserved too. In order to save a copy of the dictionary without switching to the copy, use :menuselection:`File --> Save copy`.
+
+.. |New file icon| image:: icons/filenew.png
+.. |Save file icon| image:: icons/filesave.png
+
+
+Adding images
+=============
+
+Fresh Memory supports images in the cards.
+
+
+Adding with graphical interface
+-------------------------------
+
+.. versionadded:: 1.2.0
+
+The images can be added with graphical interface by pressing :guilabel:`Add image` |Add image icon| button (:kbd:`Ctrl+G`). The application opens a file selection dialog, where it is possible to choose your image.
+
+.. |Add image icon| image:: ../images/add-image.png
+
+Adding images with graphical interface creates a subdirectory with dictionary name next to the dictionary file. All added images are saved in that subdirectory. Internally, the images are added as tags with relative path: ``<img src="%%/image.png">``.
+
+The image tags are displayed as image thumbnails, once they are entered and submitted.
+
+.. figure:: images/adding_image.png
+
+ Adding images
+
+
+Using text format
+-----------------
+
+The images can be added with the following HTML tag::
+
+ <img src="path/image.png">
+
+where the path is an absolute path to the image.
+
+A shorter path, relative to the dictionary file, can be specified with a special character "%":
+
+``<img src="%/image.png">``
+ In the same directory as the dictionary
+``<img src="%%/image.png">``
+ In a sub-directory with the dictionary name without extension, next to the dictionary file
+
+If the images use relative paths, it is safe to move the dictionary with its images to another folder.
diff --git a/userdocs/statistics.rst b/userdocs/statistics.rst
new file mode 100644
index 0000000..cc9539a
--- /dev/null
+++ b/userdocs/statistics.rst
@@ -0,0 +1,42 @@
+Cards statistics
+================
+
+.. versionadded:: 1.3.0
+
+It is possible to see detailed statistics of reviewed, scheduled and new cards. Select :menuselection:`Tools --> Statistics` in the menu (hotkey :kbd:`F7`) or click |Stats icon| :guilabel:`Statistics` tool button.
+
+.. |Stats icon| image:: ../images/statistics.png
+
+.. versionadded:: 1.4.0
+ Added the Study progress chart.
+
+The first page is |Study progress icon| :guilabel:`Study progress`. It shows a pie chart with reviewed cards, shceduled for today and new cards.
+
+.. |Study progress icon| image:: ../images/pie-chart-3d.png
+
+.. figure:: images/stats/stats_progress.png
+
+ Study progress
+
+
+|Studied cards icon| :guilabel:`Studied cards` page shows the number of cards reviewed on each day in the past. The right-most point is today, and previous days extend to the left.
+
+.. |Studied cards icon| image:: ../images/chart-past.png
+
+.. figure:: images/stats/stats_studied.png
+
+ Statistics of the studied cards
+
+By default, the displayed period is 1 week. The period can be changed with the :guilabel:`Period` combo box: 2 weeks, 4 weeks, 1 month, etc. The statistics is shown individually for each card pack. It is possible to select another card pack in :guilabel:`Card pack` combo box. The total number of reviewed cards in the selected period is shown at the very bottom of the window.
+
+Hovering the mouse over a point of the chart shows a tooltip with information about that particular day (or period): date and number of cards.
+
+.. figure:: images/stats/chart_tooltip.png
+
+|Scheduled cards icon| :guilabel:`Scheduled cards` page shows number of scheduled cards, i. e. cards to be reviewed in the future. The left-most point is today, and the following days extend to the right. This chart shows numbers of cards, which will be scheduled at the same time as today in the future days.
+
+.. |Scheduled cards icon| image:: ../images/chart-future.png
+
+.. figure:: images/stats/stats_scheduled.png
+
+ Statistics of the scheduled cards
diff --git a/userdocs/study-settings.rst b/userdocs/study-settings.rst
new file mode 100644
index 0000000..a097072
--- /dev/null
+++ b/userdocs/study-settings.rst
@@ -0,0 +1,51 @@
+Study settings
+==============
+
+The scheduling algorithm shows cards according to the following principles:
+
+* New and scheduled cards are mixed together and shown randomly.
+* In order to scatter more the scheduled cards in time, the repetition interval is randomly adjusted by a small value. This prevents scheduling similar cards in exactly the same order as they were repeated.
+* Cards studied in one day are limited. The application shows a warning, when this limit is reached. The user may continue studying more cards after this warning.
+* New cards introduced in one day are limited in order not to burden the user too much. When this limit is reached, only the scheduled cards are shown.
+
+Certain parameters of the scheduling algorithm can be changed in :menuselection:`Options --> Study settings`. See figure below.
+
+.. figure:: images/settings/study_settings.png
+
+ Study settings
+
+
+The parameters are the following:
+
+.. versionadded:: 1.4.1
+ Added "Don't add new cards after scheduled cards threshold" parameter.
+
+.. versionadded:: 1.2.0
+ Added "Day starts at" parameter.
+
+.. csv-table:: Study parameters
+ :header: "Parameter", "Default value", "Description"
+ :widths: 14, 6, 50
+
+ "Day starts at, o'clock", 3, "At what time a new day starts to correctly count today's reviewed cards and shceduled cards for today."
+ "Share of new cards", 20%, "The share of new cards to show in comparison to all shown cards."
+ "Repetition interval randomness", ± 10%, "How much the repetition interval can be randomly adjusted by the algorithm. In percents of the absolute value of the interval."
+ "Add new cards in random order", yes, "Controls if new cards are taken in random order or in the same order as they appear in the dictionary."
+ "Day reviews limit", 80, "How many card reviews should be done per day. Just shows a warning, when this limit is reached."
+ "Day limit of new cards", 10, "Maximum number of new cards shown per day. When this number is reached, only scheduled cards are shown."
+ "Don't add new cards after scheduled cards threshold", 70, "New cards will not be added, if scheduled cards for today are too many. After this threshold is reached, new cards will not be shown."
+
+
+Language of user interface
+==========================
+
+By default, Fresh Memory uses the default language of the operating system (the system locale).
+
+The following translations are available: Czech, English, Finnish, French, German, Russian, Spanish, Ukrainian.
+
+.. versionadded:: 1.3.0
+ Language can be selected manually.
+
+It is possible to manually change the language in |Language icon| :menuselection:`Options --> Language` menu. The application must be restarted to use the changed language.
+
+.. |Language icon| image:: ../images/language.png
diff --git a/userdocs/studying-cards.rst b/userdocs/studying-cards.rst
new file mode 100644
index 0000000..9ed1438
--- /dev/null
+++ b/userdocs/studying-cards.rst
@@ -0,0 +1,223 @@
+Studying cards
+==============
+
+Spaced repetition method
+------------------------
+
+Fresh Memory uses a special studying algorithm: *Time spaced repetition*, or just *Spaced repetition*. It automatically schedules repetition of cards according to their difficulty. Spaced repetition allows to quickly study any material and keep it in memory for a long time. This makes it very efficient studying method.
+
+.. note::
+ The scheduling algorithm uses the idea of `SM-2 algorithm`_ of Super Memo application.
+
+.. _SM-2 algorithm: http://www.supermemo.com/english/ol/sm2.htm
+
+According to Spaced repetition, well known cards are shown rarely, and difficult cards are shown more often. The repetitions of cards are spaced in time with *repetition intervals*. The program automatically schedules cards for repeating depending on how well they are known to the user. The better a card is known, the longer interval is automatically selected for its next repetition.
+
+The repetition intervals may range from several minutes to over a year, without a limit. The easier the card is for the user, the longer its interval becomes. If a card is difficult to remember, its repetition interval will be kept shorter so that the user would have more chance to study it.
+
+New, not learned, cards will be shown 3 times to the user on the first day. Then, it will be scheduled for repetition for the next day (interval is 1 day). The next intervals will be about 2.3 days, 5.7 days, 14 days and so on. With each card repetition, its interval automatically increases --- it will be shown more rarely.
+
+The user gives grades to the cards: good, easy, difficult, etc. This grade will affect on the calculation of the next interval.
+
+Incorrectly answered cards (grades 1 and 2) will have a very small next interval: 20 seconds or 1 minute. When previously incorrect cards are graded as correct, they will get interval of about 1 day.
+
+Spaced repetition tool
+----------------------
+
+Start Spaced repetition with :menuselection:`Tools --> Spaced repetition` |Spacedrep icon| menu item (shortcut :kbd:`F6`). It opens the study view shown below.
+
+
+.. |Spacedrep icon| image:: ../images/spaced-rep.png
+
+.. figure:: images/spacedrep/spacedrep_hidden_answer.png
+
+ Spaced repetition window --- the answer is hidden
+
+First, the study window shows a question. The answer is hidden. The user must recall the answer, then press :guilabel:`Show answer` button (shortcut :kbd:`Space`) to open the correct answer. The user must compare his answer with the correct one and evaluate himself with a grade from 1 to 5. The grades are selected with the buttons below the answer (shortcuts are keys with the same numbers). The grades depend on the card difficulty and correctness. The application then uses the grade to schedule the next card repetition.
+
+.. figure:: images/spacedrep/spacedrep.png
+
+ Spaced repetition window --- the answer is shown
+
+Grades 3, 4 and 5 mean correct answers, 1 and 2 are for an incorrect answer. Here is description of the grades:
+
+.. versionchanged:: 1.4.0
+ The grade 2 "Not completely correct" was removed. The user has enough choice to control the card intervals without it. Incorrect grades became 1 and 2.
+
+.. csv-table:: Spaced repetition grades
+ :header: Name, Description
+ :widths: 12, 55
+
+ "|Easy grade| 5 Easy", "The card is too easy, and recalled without any effort. The last interval was too short."
+ "|Good grade| 4 Good/OK", "The answer is recalled in couple of seconds. The last interval was good enough."
+ "|Difficult grade| 3 Difficult", "It's difficult to recall the answer. The last interval was too long."
+ "|Incorrect grade| 2 Incorrect", "The answer is incorrect."
+ "|Unknown grade| 1 Unknown", "Completely forgotten card, couldn't recall the answer."
+
+.. |Easy grade| image:: ../images/green-triangle-up.png
+.. |Good grade| image:: ../images/green-tick.png
+.. |Difficult grade| image:: ../images/blue-triangle-down.png
+.. |Incorrect grade| image:: ../images/red-stop.png
+.. |Unknown grade| image:: ../images/question.png
+
+If the card is new, it is decorated with a "New" label like it is shown below. Notice, there are only 4 "OK" and 5 "Easy" grades available for new cards.
+
+.. figure:: images/spacedrep/new_card.png
+
+ New card in Spaced repetition
+
+
+.. _learning-process:
+
+Learning process
+----------------
+
+In the beginning, all cards are considered *new*. New cards have not been reviewed yet. Every card must be repeated several times before it becomes *learned* and ready for the normal repetition.
+
+.. versionadded:: 1.4.0
+ Learning steps are introduced for the new cards.
+
+The whole learning process consists of *learning steps* (or levels). The first step is reviewing a new card. Next, there are two learning steps, and the last one is a learned card, which is regularly repeated. See the list of the learning steps below.
+
+.. csv-table:: Learning steps
+ :header: No., Name, Available grades, Interval, Description
+ :widths: 3, 8, 12, 13, 50
+
+ 1, "New", "OK, Easy", ---, "New, not seen, cards"
+ 2, "Unknown", "No Difficult", "20 sec / 1 min", "The second review or incorrectly graded cards"
+ 3, "Learning", "No Difficult", "10 min", "An extra repetition after several minutes"
+ 4, "Repeating", "All grades", "next day and increasing interval", "Learned card. Repeating to keep in the memory"
+
+Not all grades are available for certain learning steps. Only the highest step 4 "Repeating" uses all 5 grades. New cards have choice of 4 "OK" and 5 "Easy" grades. The steps 2 and 3 show all but the "Difficult" grade.
+
+Every new card is promoted to the next learning step, if the user gives 4 "OK" grade. The 5 "Easy" grade promotes the card two steps forward:
+
+* New cards (step 1) go directly to "Learning" step (3)
+* "Unknown" (2) cards go to "Repeating" (4)
+* "Learning" (3) cards get interval of 2 days (instead of the normal 1 day)
+
+The incorrect grades, 1 and 2, take the card back to the "Unknown" (2) learning step. This means the card will be shown again to the user in a short time. Grade 1 "Unknown" will show the card in 20 seconds, grade 2 "Incorrect" will show it in 1 minute. Then that card will normally go to the "Learning" (3) step, and to the "Repeating" (4) step, when it will be scheduled for the following day.
+
+Once the card becomes learned after reviewing it on the following day, it will use the normal increasing repetition intervals.
+
+If there are learning cards, they are shown first before other scheduled cards. If learning cards are scheduled in several minutes, but there are no other scheduled or new cards to fill the time gap, those learning cards are shown immediately.
+
+New and repeating scheduled cards are mixed together and shown randomly. The probability of showing a new card is controlled by the :guilabel:`Share of new cards` option in the study settings. The default value is 20%
+
+
+Study progress bar
+------------------
+
+.. versionchanged:: 1.4.0
+
+In the study view, the area under the card answer shows study progress for the current card pack. It displays different numeric values and a colored progress bar.
+
+.. figure:: images/spacedrep/study_progress.png
+
+.. figure:: images/spacedrep/progress_tooltip.png
+
+ Study progress in Spaced repetition
+
+The progress bar shows how many scheduled cards for today were reviewed. The green zone shows the reviewed cards. The numbers to the left of the progress bar show the reviewed cards and the total scheduled cards for today.
+
+The colored progress bar shows different types of scheduled cards in colors:
+
+Yellow
+ Learning reviews: the cards at the "Unknown" and "Learning" (2 and 3) steps
+White
+ Learned scheduled cards: at the "Repeating" (4) step
+Brown
+ New cards scheduled for today: new cards that will be shown today between the other scheduled ones
+
+Statistics for the new cards are shown above the progress bar. The number at the blue label shows new cards, which were added today. The other labels show numbers of cards in the corresponding progress bar zones.
+
+In the very beginning, all cards are new. As the user studies the new cards, the progress bar begins to show the green zone --- the reviewed cards, and the yellow zone --- the new cards, which are being learned right now. Items from the yellow zone gradually move to green (they are reviewed). Starting from the following day, the progress bar begins to show white zone: scheduled repetitions for the cards learned on the previous day.
+
+
+Exact answer
+------------
+
+It is possible to allow the user to enter an *exact answer* before seeing the correct one. This may be useful for practicing in writing foreign words.
+
+By default, the exact answer is switched off. In order to enable it, go to :menuselection:`Options --> Dictionary options`, :guilabel:`Card packs` and check the checkbox :guilabel:`Uses exact answer`. Click :guilabel:`OK`. Exact answer will be enabled for the selected card pack.
+
+.. figure:: images/spacedrep/settings_exact_answer.png
+
+ Enabling exact answer for a card pack
+
+Then each card will show an edit box to enter the user's answer before opening the correct answer.
+
+.. figure:: images/spacedrep/spacedrep_exact_answer.png
+
+ Giving an exact answer
+
+The user enters his answer and presses :guilabel:`Show answer` (:kbd:`Enter`). The window shows the user's answer and the correct answer. If the user's answer is exactly the same as the correct one, it is highlighted in green color. Otherwise, it will be red. Now the user can give the grade for his answer.
+
+.. figure:: images/spacedrep/spacedrep_exact_answer_shown_correct.png
+
+ Shown user's answer and the correct one
+
+
+Spaced repetition algorithm
+---------------------------
+
+Each card has its own *easiness*. It is a counter term for difficulty, and describes how easy is the card for the user. The program schedules all cards individually according to their easiness.
+
+When a card is reviewed, its next repetition is scheduled to occur after certain *repetition interval*. A card remembers its *current interval* --- the interval, which was last used in the scheduling.
+
+The next repetition interval is calculated with the following formula:
+
+``next_interval = interval · easiness``
+
+Each next interval is longer than the previous. Thus, the card will be shown in more increasing intervals. For example, the last interval was 3 days and the easiness is 2.5. If the easiness is not changed, the next interval is 3 · 2.5 = 7.5 days. The next after it is 7.5 · 2.5 = 18.75.
+
+In order to scatter more the scheduled cards in time, the calculated repetition interval is randomly adjusted by a small value. This prevents scheduling similar cards in exactly the same order as they were repeated. See :guilabel:`Scheduling randomness` option in the Study settings. The default probability value is ± 10%.
+
+The initial easiness for the new cards is 2.5. It may change, if the user grades the card as difficult or too easy. The minimum value for easiness is 1.3, and the maximum is 3.2.
+
+The user can give the following grades to cards:
+
+
+.. csv-table:: Card grades
+ :header: Grade, Name, Easiness, Learning step, Interval, Description
+ :widths: 3, 6, 5, 10, 10, 50
+
+ 5, "Too easy", "``+ 0.1``", "+2 steps", "will increase", "The card is too easy, and recalled without any effort. The last interval was too short."
+ 4, "Good", , "next step", "will increase", "The answer is recalled in couple of seconds. The last interval was good enough."
+ 3, "Difficult", "``- 0.17``", , "will increase", "It's difficult to recall the answer. The last interval was too long."
+ 2, "Incorrect", , "Unknown step", "1 min", "The answer is incorrect."
+ 1, "Unknown", , "Unknown step", "20 sec", "Completely forgotten card, couldn't recall the answer."
+
+The 5 "Easy" grade increases the easiness, and the next interval will be increased with larger speed than the previous one. The 4 "Good" grade doesn't change the easiness, and the next interval will increase with the same speed (easiness). The 3 "Difficult" grade decreases the easiness, and the next interval will be increased with smaller speed.
+
+There are two "incorrect" grades: 2 and 1. The 2 "Incorrect" grade shows the same card in 1 minute, so that the user can learn it once again. The easiness is not used here for the next interval calculation, but it remains the same, and will be used for next intervals when the card is learned.
+
+The 1 "Unknown" grade means the card was completely forgotten. The same card will be shown in 20 seconds.
+
+See description of *learning steps* in :ref:`learning-process` subsection.
+
+
+Card limits
+-----------
+
+Every day the user studies both new and scheduled cards. They are mixed randomly. The application limits the number of new cards introduced each day. By default, it is 10 new cards per day. After the limit for new cards is reached, only the scheduled cards are shown. The rest of the new cards are left for the following days. Thus, all new cards are gradually introduced during several days of study in order not burden the user too much. The day limit for new cards can be changed in :menuselection:`Options --> Study settings`, :guilabel:`Day limit of new cards`.
+
+It is recommended not to review too many cards per one day. The application has the day limit for all reviewed cards, 80 cards by default. It will warn the user about it when the limit is reached. Though, it is just a warning, and the user may still continue studying more cards.
+The day limit for all cards can be changed in :menuselection:`Options --> Study settings`, :guilabel:`Day reviews limit`.
+
+
+Editing cards during study
+--------------------------
+
+The cards are normally being edited in the dictionary window. But it is possible to edit the cards even in the study view. It is very convenient, when the user notes a mistake during the study. The top-right part of the study window has two buttons: :guilabel:`Delete card` |Delete card icon| (:kbd:`D`) and :guilabel:`Edit card` |Edit card icon| (:kbd:`E`). They correspondingly delete the current card and start editing it. Editing opens a separate window shown below.
+
+.. |Delete card icon| image:: icons/red-cross.png
+.. |Edit card icon| image:: icons/pencil.png
+
+.. figure:: images/spacedrep/edit_card.png
+
+ Editing card in the study
+
+
+Pressing :guilabel:`Go to dictionary window` button will take you to the dictionary view, where the current card will be centered and selected.
+
diff --git a/version.txt b/version.txt
new file mode 100644
index 0000000..bc80560
--- /dev/null
+++ b/version.txt
@@ -0,0 +1 @@
+1.5.0