From d24f813f3f2a05c112e803e4256b53535895fc98 Mon Sep 17 00:00:00 2001 From: Jedidiah Barber Date: Wed, 14 Jul 2021 11:49:10 +1200 Subject: Initial mirror commit --- .gitignore | 22 + AUTHORS | 4 + COPYING | 675 +++++++++ ChangeLog | 211 +++ Jenkinsfile | 36 + README | 244 ++++ application.qrc | 59 + copy-docs.sh | 9 + doc/chart.xcf | Bin 0 -> 83901 bytes doc/config/freshmemory.ini | 59 + doc/config/install.txt | 23 + doc/dict-format.txt | 147 ++ doc/file-associations.txt | 29 + doc/pie-chart.xcf | Bin 0 -> 94110 bytes doc/study-format.txt | 169 +++ examples/countries-europe-2.fmd | 259 ++++ examples/countries-europe-2/Albania.png | Bin 0 -> 3345 bytes examples/countries-europe-2/Andorra.png | Bin 0 -> 3337 bytes examples/countries-europe-2/Austria.png | Bin 0 -> 1682 bytes examples/countries-europe-2/Belarus.png | Bin 0 -> 2948 bytes examples/countries-europe-2/Belgium.png | Bin 0 -> 1892 bytes .../countries-europe-2/Bosnia & Herzegovina.png | Bin 0 -> 4101 bytes examples/countries-europe-2/Bulgaria.png | Bin 0 -> 1579 bytes examples/countries-europe-2/Croatia.png | Bin 0 -> 3270 bytes examples/countries-europe-2/Cyprus.png | Bin 0 -> 2485 bytes examples/countries-europe-2/Czech Republic.png | Bin 0 -> 2130 bytes examples/countries-europe-2/Denmark.png | Bin 0 -> 1803 bytes examples/countries-europe-2/Estonia.png | Bin 0 -> 1755 bytes examples/countries-europe-2/Finland.png | Bin 0 -> 1759 bytes examples/countries-europe-2/France.png | Bin 0 -> 1851 bytes examples/countries-europe-2/Germany.png | Bin 0 -> 1729 bytes examples/countries-europe-2/Great Britain.png | Bin 0 -> 5224 bytes examples/countries-europe-2/Greece.png | Bin 0 -> 2168 bytes examples/countries-europe-2/Greenland.png | Bin 0 -> 2497 bytes examples/countries-europe-2/Hungary.png | Bin 0 -> 1618 bytes examples/countries-europe-2/Iceland.png | Bin 0 -> 2002 bytes examples/countries-europe-2/Ireland.png | Bin 0 -> 1804 bytes examples/countries-europe-2/Italy.png | Bin 0 -> 1798 bytes examples/countries-europe-2/Latvia.png | Bin 0 -> 1658 bytes examples/countries-europe-2/Liechtenstein.png | Bin 0 -> 2525 bytes examples/countries-europe-2/Lithuania.png | Bin 0 -> 1717 bytes examples/countries-europe-2/Luxembourg.png | Bin 0 -> 1635 bytes examples/countries-europe-2/Macedonia.png | Bin 0 -> 4933 bytes examples/countries-europe-2/Malta.png | Bin 0 -> 2030 bytes examples/countries-europe-2/Moldova.png | Bin 0 -> 3302 bytes examples/countries-europe-2/Monaco.png | Bin 0 -> 1705 bytes examples/countries-europe-2/Montenegro.png | Bin 0 -> 3496 bytes examples/countries-europe-2/Netherlands.png | Bin 0 -> 1602 bytes examples/countries-europe-2/Norway.png | Bin 0 -> 2012 bytes examples/countries-europe-2/Poland.png | Bin 0 -> 1353 bytes examples/countries-europe-2/Portugal.png | Bin 0 -> 3583 bytes examples/countries-europe-2/Romania.png | Bin 0 -> 1886 bytes examples/countries-europe-2/Russia.png | Bin 0 -> 1558 bytes examples/countries-europe-2/San Marino.png | Bin 0 -> 4266 bytes examples/countries-europe-2/Serbia.png | Bin 0 -> 3301 bytes examples/countries-europe-2/Slovakia.png | Bin 0 -> 2857 bytes examples/countries-europe-2/Slovenia.png | Bin 0 -> 2350 bytes examples/countries-europe-2/Spain.png | Bin 0 -> 2911 bytes examples/countries-europe-2/Sweden.png | Bin 0 -> 1970 bytes examples/countries-europe-2/Switzerland.png | Bin 0 -> 1782 bytes examples/countries-europe-2/Ukraine.png | Bin 0 -> 1817 bytes freshmemory.pro | 262 ++++ freshmemory.rc | 1 + images/1downarrow.png | Bin 0 -> 722 bytes images/1leftarrow.png | Bin 0 -> 832 bytes images/1rightarrow.png | Bin 0 -> 807 bytes images/1uparrow.png | Bin 0 -> 787 bytes images/Aa.png | Bin 0 -> 762 bytes images/RX.png | Bin 0 -> 761 bytes images/add-image.png | Bin 0 -> 1118 bytes images/add.png | Bin 0 -> 1573 bytes images/attic/clock.png | Bin 0 -> 21387 bytes images/attic/large-arrow-left.png | Bin 0 -> 25627 bytes images/attic/large-arrow-right.png | Bin 0 -> 25924 bytes images/back.png | Bin 0 -> 1625 bytes images/blue-triangle-down.png | Bin 0 -> 1024 bytes images/broken-image.png | Bin 0 -> 654 bytes images/chart-future.png | Bin 0 -> 5563 bytes images/chart-past.png | Bin 0 -> 5865 bytes images/continue-search.png | Bin 0 -> 5845 bytes images/delete.png | Bin 0 -> 1607 bytes images/dic-options.png | Bin 0 -> 2675 bytes images/down.png | Bin 0 -> 1604 bytes images/editcopy.png | Bin 0 -> 879 bytes images/editcut.png | Bin 0 -> 2169 bytes images/editpaste.png | Bin 0 -> 1458 bytes images/exit.png | Bin 0 -> 1915 bytes images/fields.png | Bin 0 -> 1673 bytes images/filenew.png | Bin 0 -> 1369 bytes images/fileopen.png | Bin 0 -> 2232 bytes images/filesave.png | Bin 0 -> 1348 bytes images/filesaveas.png | Bin 0 -> 2069 bytes images/find.png | Bin 0 -> 1553 bytes images/flashcards-24.png | Bin 0 -> 1299 bytes images/font-style.png | Bin 0 -> 2185 bytes images/forward.png | Bin 0 -> 1647 bytes images/freshmemory.png | Bin 0 -> 27419 bytes images/freshmemory.svg | 165 +++ images/gplv3-88x31.png | Bin 0 -> 2666 bytes images/gray-cross.png | Bin 0 -> 646 bytes images/green-tick.png | Bin 0 -> 898 bytes images/green-triangle-up.png | Bin 0 -> 971 bytes images/info.png | Bin 0 -> 2362 bytes images/language.png | Bin 0 -> 6948 bytes images/mainicon.ico | Bin 0 -> 16958 bytes images/new-topright.png | Bin 0 -> 4032 bytes images/next.png | Bin 0 -> 2138 bytes images/openbook-24.png | Bin 0 -> 1616 bytes images/orig/Aa.xcf | Bin 0 -> 5484 bytes images/orig/RX.xcf | Bin 0 -> 8022 bytes images/orig/blue-triangle-down.xcf | Bin 0 -> 3862 bytes images/orig/card.xcf | Bin 0 -> 1600 bytes images/orig/chart-future.xcf | Bin 0 -> 12868 bytes images/orig/chart-past.xcf | Bin 0 -> 12451 bytes images/orig/green-triangle-up.xcf | Bin 0 -> 3726 bytes images/orig/question.xcf | Bin 0 -> 3555 bytes images/orig/selection.xcf | Bin 0 -> 7370 bytes images/orig/spaced-rep.xcf | Bin 0 -> 4351 bytes images/orig/whole-words.xcf | Bin 0 -> 3436 bytes images/orig/word-drill.xcf | Bin 0 -> 2467 bytes images/passes.png | Bin 0 -> 888 bytes images/pencil.png | Bin 0 -> 1296 bytes images/pie-chart-3d.png | Bin 0 -> 8979 bytes images/question.png | Bin 0 -> 1035 bytes images/red-cross.png | Bin 0 -> 1731 bytes images/red-stop.png | Bin 0 -> 2132 bytes images/remove.png | Bin 0 -> 2158 bytes images/selection.png | Bin 0 -> 534 bytes images/spaced-rep.png | Bin 0 -> 1333 bytes images/statistics.png | Bin 0 -> 942 bytes images/study-settings.png | Bin 0 -> 2760 bytes images/up.png | Bin 0 -> 1595 bytes images/warning.png | Bin 0 -> 1692 bytes images/whole-words.png | Bin 0 -> 344 bytes images/word-drill.png | Bin 0 -> 493 bytes packaging/FileAssociation.nsh | 190 +++ packaging/clean-deb.sh | 4 + packaging/create-source-archive.sh | 19 + packaging/debian/changelog.Debian | 3 + packaging/debian/conffiles | 1 + packaging/debian/control | 17 + packaging/debian/copyright | 9 + packaging/debian/freshmemory.1 | 13 + packaging/debian/freshmemory.desktop | 15 + packaging/debian/freshmemory.xml | 8 + packaging/debian/postinst | 6 + packaging/debian/prerm | 7 + packaging/freshmemory.nsi | 72 + packaging/make-deb.sh | 126 ++ packaging/make-wininstaller.bat | 15 + packaging/packaging.txt | 66 + packaging/qt-win/cleanup.bat | 2 + packaging/qt-win/copy_qtdlls.bat | 28 + packaging/qt-win/list.txt | 25 + releases/1.0.1/README | 61 + releases/1.0.1/news.txt | 1 + releases/1.0.2/README | 17 + releases/1.0.2/news.txt | 1 + releases/1.1/README | 58 + releases/1.1/news.txt | 1 + releases/1.2/README | 27 + releases/1.2/news.txt | 2 + releases/1.4.4/news.txt | 3 + releases/1.4.5/news.txt | 2 + releases/README | 74 + releases/screenshots/fm-1.0-e.png | Bin 0 -> 36991 bytes releases/screenshots/fm-1.0-f.png | Bin 0 -> 34938 bytes releases/screenshots/fm-1.1-a.png | Bin 0 -> 57121 bytes releases/screenshots/fm-1.1-b.png | Bin 0 -> 28994 bytes releases/screenshots/fm-1.1-c.png | Bin 0 -> 29042 bytes releases/screenshots/fm-1.1-d.png | Bin 0 -> 47246 bytes releases/screenshots/fm-1.1-e.png | Bin 0 -> 78414 bytes releases/screenshots/names.txt | 8 + src/charts/Chart.cpp | 23 + src/charts/Chart.h | 32 + src/charts/ChartAxes.cpp | 138 ++ src/charts/ChartAxes.h | 51 + src/charts/ChartDataLine.cpp | 80 ++ src/charts/ChartDataLine.h | 37 + src/charts/ChartMarker.cpp | 50 + src/charts/ChartMarker.h | 36 + src/charts/ChartScene.cpp | 111 ++ src/charts/ChartScene.h | 55 + src/charts/ChartToolTip.cpp | 43 + src/charts/ChartToolTip.h | 26 + src/charts/ChartView.cpp | 12 + src/charts/ChartView.h | 16 + src/charts/DataPoint.h | 15 + src/charts/PieChart.cpp | 28 + src/charts/PieChart.h | 27 + src/charts/PieChartScene.cpp | 30 + src/charts/PieChartScene.h | 30 + src/charts/PieLegend.cpp | 48 + src/charts/PieLegend.h | 34 + src/charts/PieRound.cpp | 77 + src/charts/PieRound.h | 37 + src/charts/TimeChart.cpp | 155 ++ src/charts/TimeChart.h | 54 + src/dic-options/DictionaryOptionsDialog.cpp | 62 + src/dic-options/DictionaryOptionsDialog.h | 41 + src/dic-options/DraggableListModel.cpp | 83 ++ src/dic-options/DraggableListModel.h | 33 + src/dic-options/FieldStyleDelegate.cpp | 35 + src/dic-options/FieldStyleDelegate.h | 19 + src/dic-options/FieldsListModel.cpp | 222 +++ src/dic-options/FieldsListModel.h | 49 + src/dic-options/FieldsPage.cpp | 139 ++ src/dic-options/FieldsPage.h | 37 + src/dic-options/FieldsPreviewModel.cpp | 34 + src/dic-options/FieldsPreviewModel.h | 27 + src/dic-options/FieldsView.cpp | 41 + src/dic-options/FieldsView.h | 24 + src/dic-options/PackFieldsListModel.cpp | 101 ++ src/dic-options/PackFieldsListModel.h | 34 + src/dic-options/PackFieldsView.cpp | 36 + src/dic-options/PackFieldsView.h | 24 + src/dic-options/PacksListModel.cpp | 81 ++ src/dic-options/PacksListModel.h | 32 + src/dic-options/PacksPage.cpp | 345 +++++ src/dic-options/PacksPage.h | 70 + src/dic-options/UnusedFieldsListModel.cpp | 111 ++ src/dic-options/UnusedFieldsListModel.h | 35 + src/dictionary/Card.cpp | 125 ++ src/dictionary/Card.h | 50 + src/dictionary/CardPack.cpp | 432 ++++++ src/dictionary/CardPack.h | 144 ++ src/dictionary/DicCsvReader.cpp | 205 +++ src/dictionary/DicCsvReader.h | 41 + src/dictionary/DicCsvWriter.cpp | 110 ++ src/dictionary/DicCsvWriter.h | 31 + src/dictionary/DicRecord.cpp | 62 + src/dictionary/DicRecord.h | 40 + src/dictionary/Dictionary.cpp | 601 ++++++++ src/dictionary/Dictionary.h | 187 +++ src/dictionary/DictionaryReader.cpp | 384 +++++ src/dictionary/DictionaryReader.h | 44 + src/dictionary/DictionaryWriter.cpp | 79 + src/dictionary/DictionaryWriter.h | 26 + src/dictionary/Field.cpp | 18 + src/dictionary/Field.h | 46 + src/dictionary/ICardPack.cpp | 23 + src/dictionary/ICardPack.h | 33 + src/dictionary/IDictionary.cpp | 59 + src/dictionary/IDictionary.h | 51 + src/dictionary/TreeItem.h | 25 + src/export-import/CsvData.h | 36 + src/export-import/CsvDialog.cpp | 176 +++ src/export-import/CsvDialog.h | 62 + src/export-import/CsvExportDialog.cpp | 137 ++ src/export-import/CsvExportDialog.h | 52 + src/export-import/CsvImportDialog.cpp | 155 ++ src/export-import/CsvImportDialog.h | 64 + src/field-styles/FieldStyle.cpp | 38 + src/field-styles/FieldStyle.h | 28 + src/field-styles/FieldStyleFactory.cpp | 145 ++ src/field-styles/FieldStyleFactory.h | 49 + src/main-view/AboutDialog.cpp | 27 + src/main-view/AboutDialog.h | 17 + src/main-view/AppModel.cpp | 196 +++ src/main-view/AppModel.h | 64 + src/main-view/CardFilterModel.cpp | 24 + src/main-view/CardFilterModel.h | 21 + src/main-view/CardPreview.cpp | 55 + src/main-view/CardPreview.h | 30 + src/main-view/DictTableDelegate.cpp | 128 ++ src/main-view/DictTableDelegate.h | 44 + src/main-view/DictTableDelegatePainter.cpp | 61 + src/main-view/DictTableDelegatePainter.h | 36 + src/main-view/DictTableModel.cpp | 142 ++ src/main-view/DictTableModel.h | 52 + src/main-view/DictTableView.cpp | 84 ++ src/main-view/DictTableView.h | 36 + src/main-view/DictionaryTabWidget.cpp | 108 ++ src/main-view/DictionaryTabWidget.h | 46 + src/main-view/FieldContentCodec.cpp | 52 + src/main-view/FieldContentCodec.h | 28 + src/main-view/FieldContentPainter.h | 16 + src/main-view/FindPanel.cpp | 258 ++++ src/main-view/FindPanel.h | 66 + src/main-view/LanguageMenu.cpp | 64 + src/main-view/LanguageMenu.h | 28 + src/main-view/MainWindow.cpp | 1313 +++++++++++++++++ src/main-view/MainWindow.h | 218 +++ src/main-view/PacksTreeModel.cpp | 93 ++ src/main-view/PacksTreeModel.h | 32 + src/main-view/RecentFilesManager.cpp | 99 ++ src/main-view/RecentFilesManager.h | 43 + src/main-view/RecordEditor.cpp | 122 ++ src/main-view/RecordEditor.h | 46 + src/main-view/UndoCommands.cpp | 322 +++++ src/main-view/UndoCommands.h | 109 ++ src/main-view/WelcomeScreen.cpp | 51 + src/main-view/WelcomeScreen.h | 25 + src/main.cpp | 102 ++ src/main.h | 8 + src/settings/ColorBox.cpp | 36 + src/settings/ColorBox.h | 28 + src/settings/FontColorSettingsDialog.cpp | 300 ++++ src/settings/FontColorSettingsDialog.h | 71 + src/settings/StudySettingsDialog.cpp | 141 ++ src/settings/StudySettingsDialog.h | 45 + src/settings/StylePreviewModel.cpp | 63 + src/settings/StylePreviewModel.h | 32 + src/settings/StylesListModel.h | 16 + src/statistics/BaseStatPage.cpp | 38 + src/statistics/BaseStatPage.h | 31 + src/statistics/ProgressPage.cpp | 41 + src/statistics/ProgressPage.h | 33 + src/statistics/ScheduledPage.cpp | 35 + src/statistics/ScheduledPage.h | 22 + src/statistics/StatisticsParams.cpp | 2 + src/statistics/StatisticsParams.h | 20 + src/statistics/StatisticsView.cpp | 182 +++ src/statistics/StatisticsView.h | 53 + src/statistics/StudiedPage.cpp | 17 + src/statistics/StudiedPage.h | 18 + src/statistics/TimeChartPage.cpp | 54 + src/statistics/TimeChartPage.h | 33 + src/strings.cpp | 6 + src/strings.h | 22 + src/study/CardEditDialog.cpp | 119 ++ src/study/CardEditDialog.h | 42 + src/study/CardSideView.cpp | 205 +++ src/study/CardSideView.h | 52 + src/study/CardsStatusBar.cpp | 82 ++ src/study/CardsStatusBar.h | 37 + src/study/IStudyModel.cpp | 9 + src/study/IStudyModel.h | 35 + src/study/IStudyWindow.cpp | 293 ++++ src/study/IStudyWindow.h | 107 ++ src/study/NumberFrame.cpp | 40 + src/study/NumberFrame.h | 29 + src/study/SpacedRepetitionModel.cpp | 390 +++++ src/study/SpacedRepetitionModel.h | 79 + src/study/SpacedRepetitionWindow.cpp | 380 +++++ src/study/SpacedRepetitionWindow.h | 89 ++ src/study/StudyFileReader.cpp | 238 ++++ src/study/StudyFileReader.h | 44 + src/study/StudyFileWriter.cpp | 68 + src/study/StudyFileWriter.h | 26 + src/study/StudyRecord.cpp | 129 ++ src/study/StudyRecord.h | 62 + src/study/StudySettings.cpp | 93 ++ src/study/StudySettings.h | 40 + src/study/WarningPanel.cpp | 49 + src/study/WarningPanel.h | 25 + src/study/WordDrillModel.cpp | 163 +++ src/study/WordDrillModel.h | 62 + src/study/WordDrillWindow.cpp | 180 +++ src/study/WordDrillWindow.h | 46 + src/utils/IRandomGenerator.h | 17 + src/utils/RandomGenerator.cpp | 38 + src/utils/RandomGenerator.h | 16 + src/utils/TimeProvider.cpp | 6 + src/utils/TimeProvider.h | 12 + src/version.cpp | 3 + src/version.h | 19 + tests/common/RecordsParam.cpp | 51 + tests/common/RecordsParam.h | 58 + tests/common/RecordsParam_create.cpp | 62 + tests/common/printQtTypes.cpp | 31 + tests/common/printQtTypes.h | 23 + tests/fute/charts/charts.pro | 35 + tests/fute/charts/charts_test.cpp | 52 + tests/fute/charts/charts_test.h | 31 + tests/fute/charts/main.cpp | 12 + tests/fute/pieCharts/main.cpp | 12 + tests/fute/pieCharts/pieCharts.pro | 29 + tests/fute/pieCharts/pieCharts_test.cpp | 52 + tests/fute/pieCharts/pieCharts_test.h | 34 + tests/fute/timeCharts/main.cpp | 12 + tests/fute/timeCharts/timeCharts.pro | 35 + tests/fute/timeCharts/timeCharts_test.cpp | 58 + tests/fute/timeCharts/timeCharts_test.h | 30 + tests/mocks/CardPack_mock.cpp | 31 + tests/mocks/CardPack_mock.h | 23 + tests/mocks/Dictionary_mock.cpp | 6 + tests/mocks/Dictionary_mock.h | 32 + tests/mocks/RandomGenerator_mock.h | 33 + tests/mocks/TimeProvider_mock.cpp | 9 + tests/unit/Card/Card_GenerateAnswers_test.cpp | 66 + tests/unit/Card/Card_GenerateAnswers_test.h | 34 + tests/unit/Card/Card_test.cpp | 36 + tests/unit/Card/Card_test.h | 18 + tests/unit/Card/Card_test_QuestionAnswer.cpp | 30 + tests/unit/Card/card.pri | 10 + .../unit/CardPack/CardPack_GenerateCards_test.cpp | 43 + tests/unit/CardPack/CardPack_GenerateCards_test.h | 26 + tests/unit/CardPack/CardPack_test.cpp | 80 ++ tests/unit/CardPack/CardPack_test.h | 31 + tests/unit/CardPack/cPack.pri | 7 + tests/unit/CardSideView/CardSideView_test.cpp | 49 + tests/unit/CardSideView/CardSideView_test.h | 21 + tests/unit/CardSideView/csView.pri | 8 + .../unit/RandomGenerator/RandomGenerator_test.cpp | 79 + tests/unit/RandomGenerator/rndGen.pri | 6 + tests/unit/Settings/FieldStyleFactory_test.cpp | 88 ++ tests/unit/Settings/FieldStyleFactory_test.h | 29 + tests/unit/Settings/StudySettings_test.cpp | 42 + tests/unit/Settings/StudySettings_test.h | 22 + tests/unit/Settings/TestSettings.cpp | 16 + tests/unit/Settings/TestSettings.h | 11 + tests/unit/Settings/set.pri | 10 + .../SRModel_pickCard_test.cpp | 420 ++++++ .../SpacedRepetitionModel/SRModel_pickCard_test.h | 27 + .../SRModel_schedule_test.cpp | 304 ++++ .../SpacedRepetitionModel/SRModel_schedule_test.h | 24 + .../SRModel_showGrades_test.cpp | 36 + .../SRModel_showGrades_test.h | 14 + tests/unit/SpacedRepetitionModel/SRModel_test.cpp | 67 + tests/unit/SpacedRepetitionModel/SRModel_test.h | 46 + tests/unit/SpacedRepetitionModel/srModel.pri | 16 + tests/unit/cards.pri | 24 + tests/unit/common.pri | 3 + tests/unit/main.cpp | 9 + tests/unit/random.pri | 1 + tests/unit/studySets.pri | 9 + tests/unit/unit_tests.pro | 29 + tr/freshmemory_cs.ts | 1505 +++++++++++++++++++ tr/freshmemory_de.ts | 1501 +++++++++++++++++++ tr/freshmemory_en.ts | 1495 +++++++++++++++++++ tr/freshmemory_es.ts | 1502 +++++++++++++++++++ tr/freshmemory_fi.ts | 1498 +++++++++++++++++++ tr/freshmemory_fr.ts | 1500 +++++++++++++++++++ tr/freshmemory_ru.ts | 1507 ++++++++++++++++++++ tr/freshmemory_uk.ts | 1504 +++++++++++++++++++ userdocs/_static/default.css | 498 +++++++ userdocs/_static/icon-note.png | Bin 0 -> 2040 bytes userdocs/_static/icon-tip.png | Bin 0 -> 3320 bytes userdocs/_templates/layout.html | 10 + userdocs/_templates/version.html | 2 + userdocs/about.md | 28 + userdocs/activation.rst | 92 ++ userdocs/browsing-cards.rst | 65 + userdocs/cards-generation.rst | 30 + userdocs/conf.py | 259 ++++ userdocs/diagrams/cards_generation.dia | Bin 0 -> 2344 bytes userdocs/diagrams/records.png | Bin 0 -> 2949 bytes userdocs/dictionary-and-study.rst | 29 + userdocs/dictionary-options.rst | 90 ++ userdocs/icons/1downarrow.png | Bin 0 -> 722 bytes userdocs/icons/1uparrow.png | Bin 0 -> 787 bytes userdocs/icons/add.png | Bin 0 -> 1573 bytes userdocs/icons/delete.png | Bin 0 -> 1607 bytes userdocs/icons/down.png | Bin 0 -> 1604 bytes userdocs/icons/filenew.png | Bin 0 -> 1369 bytes userdocs/icons/filesave.png | Bin 0 -> 1348 bytes userdocs/icons/passes.png | Bin 0 -> 888 bytes userdocs/icons/pencil.png | Bin 0 -> 1296 bytes userdocs/icons/red-cross.png | Bin 0 -> 1731 bytes userdocs/icons/up.png | Bin 0 -> 1595 bytes userdocs/images/activation/about_activated.png | Bin 0 -> 60505 bytes userdocs/images/activation/about_trial.png | Bin 0 -> 61417 bytes userdocs/images/activation/activation_dialog.png | Bin 0 -> 14943 bytes .../activation/activation_dialog_activated.png | Bin 0 -> 15294 bytes .../images/activation/activation_dialog_trial.png | Bin 0 -> 14714 bytes userdocs/images/activation/trial_reminder.png | Bin 0 -> 21986 bytes userdocs/images/adding_image.png | Bin 0 -> 14337 bytes userdocs/images/browsing.png | Bin 0 -> 63411 bytes userdocs/images/browsing2.png | Bin 0 -> 79382 bytes userdocs/images/cards_generation.png | Bin 0 -> 16442 bytes userdocs/images/settings/field_options.png | Bin 0 -> 41689 bytes userdocs/images/settings/field_style_options.png | Bin 0 -> 41600 bytes userdocs/images/settings/keyword.png | Bin 0 -> 15399 bytes userdocs/images/settings/pack_options.png | Bin 0 -> 44392 bytes userdocs/images/settings/study_settings.png | Bin 0 -> 20556 bytes userdocs/images/spacedrep/edit_card.png | Bin 0 -> 19802 bytes userdocs/images/spacedrep/new_card.png | Bin 0 -> 28182 bytes userdocs/images/spacedrep/progress_tooltip.png | Bin 0 -> 6306 bytes .../images/spacedrep/settings_exact_answer.png | Bin 0 -> 46651 bytes userdocs/images/spacedrep/spacedrep.png | Bin 0 -> 26449 bytes .../images/spacedrep/spacedrep_exact_answer.png | Bin 0 -> 26470 bytes .../spacedrep_exact_answer_shown_correct.png | Bin 0 -> 27089 bytes .../images/spacedrep/spacedrep_hidden_answer.png | Bin 0 -> 26845 bytes userdocs/images/spacedrep/study_progress.png | Bin 0 -> 3558 bytes userdocs/images/ss-new_dictionary.png | Bin 0 -> 7451 bytes userdocs/images/ss-word_drill_back_enabled.png | Bin 0 -> 11861 bytes userdocs/images/ss-word_drill_back_pressed.png | Bin 0 -> 11282 bytes userdocs/images/ss-word_drill_history.png | Bin 0 -> 23495 bytes userdocs/images/ss-word_drill_second_cycle.png | Bin 0 -> 5095 bytes userdocs/images/stats/chart_tooltip.png | Bin 0 -> 8128 bytes userdocs/images/stats/stats_progress.png | Bin 0 -> 46119 bytes userdocs/images/stats/stats_scheduled.png | Bin 0 -> 46220 bytes userdocs/images/stats/stats_studied.png | Bin 0 -> 50293 bytes userdocs/images/welcome_panel.png | Bin 0 -> 22740 bytes userdocs/images/word_drill.png | Bin 0 -> 30388 bytes userdocs/index.rst | 19 + userdocs/introduction.rst | 97 ++ userdocs/statistics.rst | 42 + userdocs/study-settings.rst | 51 + userdocs/studying-cards.rst | 223 +++ version.txt | 1 + 492 files changed, 34992 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 Jenkinsfile create mode 100644 README create mode 100644 application.qrc create mode 100755 copy-docs.sh create mode 100644 doc/chart.xcf create mode 100644 doc/config/freshmemory.ini create mode 100644 doc/config/install.txt create mode 100644 doc/dict-format.txt create mode 100644 doc/file-associations.txt create mode 100644 doc/pie-chart.xcf create mode 100644 doc/study-format.txt create mode 100644 examples/countries-europe-2.fmd create mode 100644 examples/countries-europe-2/Albania.png create mode 100644 examples/countries-europe-2/Andorra.png create mode 100644 examples/countries-europe-2/Austria.png create mode 100644 examples/countries-europe-2/Belarus.png create mode 100644 examples/countries-europe-2/Belgium.png create mode 100644 examples/countries-europe-2/Bosnia & Herzegovina.png create mode 100644 examples/countries-europe-2/Bulgaria.png create mode 100644 examples/countries-europe-2/Croatia.png create mode 100644 examples/countries-europe-2/Cyprus.png create mode 100644 examples/countries-europe-2/Czech Republic.png create mode 100644 examples/countries-europe-2/Denmark.png create mode 100644 examples/countries-europe-2/Estonia.png create mode 100644 examples/countries-europe-2/Finland.png create mode 100644 examples/countries-europe-2/France.png create mode 100644 examples/countries-europe-2/Germany.png create mode 100644 examples/countries-europe-2/Great Britain.png create mode 100644 examples/countries-europe-2/Greece.png create mode 100644 examples/countries-europe-2/Greenland.png create mode 100644 examples/countries-europe-2/Hungary.png create mode 100644 examples/countries-europe-2/Iceland.png create mode 100644 examples/countries-europe-2/Ireland.png create mode 100644 examples/countries-europe-2/Italy.png create mode 100644 examples/countries-europe-2/Latvia.png create mode 100644 examples/countries-europe-2/Liechtenstein.png create mode 100644 examples/countries-europe-2/Lithuania.png create mode 100644 examples/countries-europe-2/Luxembourg.png create mode 100644 examples/countries-europe-2/Macedonia.png create mode 100644 examples/countries-europe-2/Malta.png create mode 100644 examples/countries-europe-2/Moldova.png create mode 100644 examples/countries-europe-2/Monaco.png create mode 100644 examples/countries-europe-2/Montenegro.png create mode 100644 examples/countries-europe-2/Netherlands.png create mode 100644 examples/countries-europe-2/Norway.png create mode 100644 examples/countries-europe-2/Poland.png create mode 100644 examples/countries-europe-2/Portugal.png create mode 100644 examples/countries-europe-2/Romania.png create mode 100644 examples/countries-europe-2/Russia.png create mode 100644 examples/countries-europe-2/San Marino.png create mode 100644 examples/countries-europe-2/Serbia.png create mode 100644 examples/countries-europe-2/Slovakia.png create mode 100644 examples/countries-europe-2/Slovenia.png create mode 100644 examples/countries-europe-2/Spain.png create mode 100644 examples/countries-europe-2/Sweden.png create mode 100644 examples/countries-europe-2/Switzerland.png create mode 100644 examples/countries-europe-2/Ukraine.png create mode 100644 freshmemory.pro create mode 100644 freshmemory.rc create mode 100644 images/1downarrow.png create mode 100644 images/1leftarrow.png create mode 100644 images/1rightarrow.png create mode 100644 images/1uparrow.png create mode 100644 images/Aa.png create mode 100644 images/RX.png create mode 100644 images/add-image.png create mode 100644 images/add.png create mode 100644 images/attic/clock.png create mode 100644 images/attic/large-arrow-left.png create mode 100644 images/attic/large-arrow-right.png create mode 100644 images/back.png create mode 100644 images/blue-triangle-down.png create mode 100644 images/broken-image.png create mode 100644 images/chart-future.png create mode 100644 images/chart-past.png create mode 100644 images/continue-search.png create mode 100644 images/delete.png create mode 100644 images/dic-options.png create mode 100644 images/down.png create mode 100644 images/editcopy.png create mode 100644 images/editcut.png create mode 100644 images/editpaste.png create mode 100644 images/exit.png create mode 100644 images/fields.png create mode 100644 images/filenew.png create mode 100644 images/fileopen.png create mode 100644 images/filesave.png create mode 100644 images/filesaveas.png create mode 100644 images/find.png create mode 100644 images/flashcards-24.png create mode 100644 images/font-style.png create mode 100644 images/forward.png create mode 100644 images/freshmemory.png create mode 100644 images/freshmemory.svg create mode 100644 images/gplv3-88x31.png create mode 100644 images/gray-cross.png create mode 100644 images/green-tick.png create mode 100644 images/green-triangle-up.png create mode 100644 images/info.png create mode 100644 images/language.png create mode 100644 images/mainicon.ico create mode 100644 images/new-topright.png create mode 100644 images/next.png create mode 100644 images/openbook-24.png create mode 100644 images/orig/Aa.xcf create mode 100644 images/orig/RX.xcf create mode 100644 images/orig/blue-triangle-down.xcf create mode 100644 images/orig/card.xcf create mode 100644 images/orig/chart-future.xcf create mode 100644 images/orig/chart-past.xcf create mode 100644 images/orig/green-triangle-up.xcf create mode 100644 images/orig/question.xcf create mode 100644 images/orig/selection.xcf create mode 100644 images/orig/spaced-rep.xcf create mode 100644 images/orig/whole-words.xcf create mode 100644 images/orig/word-drill.xcf create mode 100644 images/passes.png create mode 100644 images/pencil.png create mode 100644 images/pie-chart-3d.png create mode 100644 images/question.png create mode 100644 images/red-cross.png create mode 100644 images/red-stop.png create mode 100644 images/remove.png create mode 100644 images/selection.png create mode 100644 images/spaced-rep.png create mode 100644 images/statistics.png create mode 100644 images/study-settings.png create mode 100644 images/up.png create mode 100644 images/warning.png create mode 100644 images/whole-words.png create mode 100644 images/word-drill.png create mode 100644 packaging/FileAssociation.nsh create mode 100755 packaging/clean-deb.sh create mode 100755 packaging/create-source-archive.sh create mode 100644 packaging/debian/changelog.Debian create mode 100644 packaging/debian/conffiles create mode 100644 packaging/debian/control create mode 100644 packaging/debian/copyright create mode 100644 packaging/debian/freshmemory.1 create mode 100644 packaging/debian/freshmemory.desktop create mode 100644 packaging/debian/freshmemory.xml create mode 100755 packaging/debian/postinst create mode 100755 packaging/debian/prerm create mode 100644 packaging/freshmemory.nsi create mode 100755 packaging/make-deb.sh create mode 100644 packaging/make-wininstaller.bat create mode 100644 packaging/packaging.txt create mode 100644 packaging/qt-win/cleanup.bat create mode 100644 packaging/qt-win/copy_qtdlls.bat create mode 100644 packaging/qt-win/list.txt create mode 100644 releases/1.0.1/README create mode 100644 releases/1.0.1/news.txt create mode 100644 releases/1.0.2/README create mode 100644 releases/1.0.2/news.txt create mode 100644 releases/1.1/README create mode 100644 releases/1.1/news.txt create mode 100644 releases/1.2/README create mode 100644 releases/1.2/news.txt create mode 100644 releases/1.4.4/news.txt create mode 100644 releases/1.4.5/news.txt create mode 100644 releases/README create mode 100644 releases/screenshots/fm-1.0-e.png create mode 100644 releases/screenshots/fm-1.0-f.png create mode 100644 releases/screenshots/fm-1.1-a.png create mode 100644 releases/screenshots/fm-1.1-b.png create mode 100644 releases/screenshots/fm-1.1-c.png create mode 100644 releases/screenshots/fm-1.1-d.png create mode 100644 releases/screenshots/fm-1.1-e.png create mode 100644 releases/screenshots/names.txt create mode 100644 src/charts/Chart.cpp create mode 100644 src/charts/Chart.h create mode 100644 src/charts/ChartAxes.cpp create mode 100644 src/charts/ChartAxes.h create mode 100644 src/charts/ChartDataLine.cpp create mode 100644 src/charts/ChartDataLine.h create mode 100644 src/charts/ChartMarker.cpp create mode 100644 src/charts/ChartMarker.h create mode 100644 src/charts/ChartScene.cpp create mode 100644 src/charts/ChartScene.h create mode 100644 src/charts/ChartToolTip.cpp create mode 100644 src/charts/ChartToolTip.h create mode 100644 src/charts/ChartView.cpp create mode 100644 src/charts/ChartView.h create mode 100644 src/charts/DataPoint.h create mode 100644 src/charts/PieChart.cpp create mode 100644 src/charts/PieChart.h create mode 100644 src/charts/PieChartScene.cpp create mode 100644 src/charts/PieChartScene.h create mode 100644 src/charts/PieLegend.cpp create mode 100644 src/charts/PieLegend.h create mode 100644 src/charts/PieRound.cpp create mode 100644 src/charts/PieRound.h create mode 100644 src/charts/TimeChart.cpp create mode 100644 src/charts/TimeChart.h create mode 100644 src/dic-options/DictionaryOptionsDialog.cpp create mode 100644 src/dic-options/DictionaryOptionsDialog.h create mode 100644 src/dic-options/DraggableListModel.cpp create mode 100644 src/dic-options/DraggableListModel.h create mode 100644 src/dic-options/FieldStyleDelegate.cpp create mode 100644 src/dic-options/FieldStyleDelegate.h create mode 100644 src/dic-options/FieldsListModel.cpp create mode 100644 src/dic-options/FieldsListModel.h create mode 100644 src/dic-options/FieldsPage.cpp create mode 100644 src/dic-options/FieldsPage.h create mode 100644 src/dic-options/FieldsPreviewModel.cpp create mode 100644 src/dic-options/FieldsPreviewModel.h create mode 100644 src/dic-options/FieldsView.cpp create mode 100644 src/dic-options/FieldsView.h create mode 100644 src/dic-options/PackFieldsListModel.cpp create mode 100644 src/dic-options/PackFieldsListModel.h create mode 100644 src/dic-options/PackFieldsView.cpp create mode 100644 src/dic-options/PackFieldsView.h create mode 100644 src/dic-options/PacksListModel.cpp create mode 100644 src/dic-options/PacksListModel.h create mode 100644 src/dic-options/PacksPage.cpp create mode 100644 src/dic-options/PacksPage.h create mode 100644 src/dic-options/UnusedFieldsListModel.cpp create mode 100644 src/dic-options/UnusedFieldsListModel.h create mode 100644 src/dictionary/Card.cpp create mode 100644 src/dictionary/Card.h create mode 100644 src/dictionary/CardPack.cpp create mode 100644 src/dictionary/CardPack.h create mode 100644 src/dictionary/DicCsvReader.cpp create mode 100644 src/dictionary/DicCsvReader.h create mode 100644 src/dictionary/DicCsvWriter.cpp create mode 100644 src/dictionary/DicCsvWriter.h create mode 100644 src/dictionary/DicRecord.cpp create mode 100644 src/dictionary/DicRecord.h create mode 100644 src/dictionary/Dictionary.cpp create mode 100644 src/dictionary/Dictionary.h create mode 100644 src/dictionary/DictionaryReader.cpp create mode 100644 src/dictionary/DictionaryReader.h create mode 100644 src/dictionary/DictionaryWriter.cpp create mode 100644 src/dictionary/DictionaryWriter.h create mode 100644 src/dictionary/Field.cpp create mode 100644 src/dictionary/Field.h create mode 100644 src/dictionary/ICardPack.cpp create mode 100644 src/dictionary/ICardPack.h create mode 100644 src/dictionary/IDictionary.cpp create mode 100644 src/dictionary/IDictionary.h create mode 100644 src/dictionary/TreeItem.h create mode 100644 src/export-import/CsvData.h create mode 100644 src/export-import/CsvDialog.cpp create mode 100644 src/export-import/CsvDialog.h create mode 100644 src/export-import/CsvExportDialog.cpp create mode 100644 src/export-import/CsvExportDialog.h create mode 100644 src/export-import/CsvImportDialog.cpp create mode 100644 src/export-import/CsvImportDialog.h create mode 100644 src/field-styles/FieldStyle.cpp create mode 100644 src/field-styles/FieldStyle.h create mode 100644 src/field-styles/FieldStyleFactory.cpp create mode 100644 src/field-styles/FieldStyleFactory.h create mode 100644 src/main-view/AboutDialog.cpp create mode 100644 src/main-view/AboutDialog.h create mode 100644 src/main-view/AppModel.cpp create mode 100644 src/main-view/AppModel.h create mode 100644 src/main-view/CardFilterModel.cpp create mode 100644 src/main-view/CardFilterModel.h create mode 100644 src/main-view/CardPreview.cpp create mode 100644 src/main-view/CardPreview.h create mode 100644 src/main-view/DictTableDelegate.cpp create mode 100644 src/main-view/DictTableDelegate.h create mode 100644 src/main-view/DictTableDelegatePainter.cpp create mode 100644 src/main-view/DictTableDelegatePainter.h create mode 100644 src/main-view/DictTableModel.cpp create mode 100644 src/main-view/DictTableModel.h create mode 100644 src/main-view/DictTableView.cpp create mode 100644 src/main-view/DictTableView.h create mode 100644 src/main-view/DictionaryTabWidget.cpp create mode 100644 src/main-view/DictionaryTabWidget.h create mode 100644 src/main-view/FieldContentCodec.cpp create mode 100644 src/main-view/FieldContentCodec.h create mode 100644 src/main-view/FieldContentPainter.h create mode 100644 src/main-view/FindPanel.cpp create mode 100644 src/main-view/FindPanel.h create mode 100644 src/main-view/LanguageMenu.cpp create mode 100644 src/main-view/LanguageMenu.h create mode 100644 src/main-view/MainWindow.cpp create mode 100644 src/main-view/MainWindow.h create mode 100644 src/main-view/PacksTreeModel.cpp create mode 100644 src/main-view/PacksTreeModel.h create mode 100644 src/main-view/RecentFilesManager.cpp create mode 100644 src/main-view/RecentFilesManager.h create mode 100644 src/main-view/RecordEditor.cpp create mode 100644 src/main-view/RecordEditor.h create mode 100644 src/main-view/UndoCommands.cpp create mode 100644 src/main-view/UndoCommands.h create mode 100644 src/main-view/WelcomeScreen.cpp create mode 100644 src/main-view/WelcomeScreen.h create mode 100644 src/main.cpp create mode 100644 src/main.h create mode 100644 src/settings/ColorBox.cpp create mode 100644 src/settings/ColorBox.h create mode 100644 src/settings/FontColorSettingsDialog.cpp create mode 100644 src/settings/FontColorSettingsDialog.h create mode 100644 src/settings/StudySettingsDialog.cpp create mode 100644 src/settings/StudySettingsDialog.h create mode 100644 src/settings/StylePreviewModel.cpp create mode 100644 src/settings/StylePreviewModel.h create mode 100644 src/settings/StylesListModel.h create mode 100644 src/statistics/BaseStatPage.cpp create mode 100644 src/statistics/BaseStatPage.h create mode 100644 src/statistics/ProgressPage.cpp create mode 100644 src/statistics/ProgressPage.h create mode 100644 src/statistics/ScheduledPage.cpp create mode 100644 src/statistics/ScheduledPage.h create mode 100644 src/statistics/StatisticsParams.cpp create mode 100644 src/statistics/StatisticsParams.h create mode 100644 src/statistics/StatisticsView.cpp create mode 100644 src/statistics/StatisticsView.h create mode 100644 src/statistics/StudiedPage.cpp create mode 100644 src/statistics/StudiedPage.h create mode 100644 src/statistics/TimeChartPage.cpp create mode 100644 src/statistics/TimeChartPage.h create mode 100644 src/strings.cpp create mode 100644 src/strings.h create mode 100644 src/study/CardEditDialog.cpp create mode 100644 src/study/CardEditDialog.h create mode 100644 src/study/CardSideView.cpp create mode 100644 src/study/CardSideView.h create mode 100644 src/study/CardsStatusBar.cpp create mode 100644 src/study/CardsStatusBar.h create mode 100644 src/study/IStudyModel.cpp create mode 100644 src/study/IStudyModel.h create mode 100644 src/study/IStudyWindow.cpp create mode 100644 src/study/IStudyWindow.h create mode 100644 src/study/NumberFrame.cpp create mode 100644 src/study/NumberFrame.h create mode 100644 src/study/SpacedRepetitionModel.cpp create mode 100644 src/study/SpacedRepetitionModel.h create mode 100644 src/study/SpacedRepetitionWindow.cpp create mode 100644 src/study/SpacedRepetitionWindow.h create mode 100644 src/study/StudyFileReader.cpp create mode 100644 src/study/StudyFileReader.h create mode 100644 src/study/StudyFileWriter.cpp create mode 100644 src/study/StudyFileWriter.h create mode 100644 src/study/StudyRecord.cpp create mode 100644 src/study/StudyRecord.h create mode 100644 src/study/StudySettings.cpp create mode 100644 src/study/StudySettings.h create mode 100644 src/study/WarningPanel.cpp create mode 100644 src/study/WarningPanel.h create mode 100644 src/study/WordDrillModel.cpp create mode 100644 src/study/WordDrillModel.h create mode 100644 src/study/WordDrillWindow.cpp create mode 100644 src/study/WordDrillWindow.h create mode 100644 src/utils/IRandomGenerator.h create mode 100644 src/utils/RandomGenerator.cpp create mode 100644 src/utils/RandomGenerator.h create mode 100644 src/utils/TimeProvider.cpp create mode 100644 src/utils/TimeProvider.h create mode 100644 src/version.cpp create mode 100644 src/version.h create mode 100644 tests/common/RecordsParam.cpp create mode 100644 tests/common/RecordsParam.h create mode 100644 tests/common/RecordsParam_create.cpp create mode 100644 tests/common/printQtTypes.cpp create mode 100644 tests/common/printQtTypes.h create mode 100644 tests/fute/charts/charts.pro create mode 100644 tests/fute/charts/charts_test.cpp create mode 100644 tests/fute/charts/charts_test.h create mode 100644 tests/fute/charts/main.cpp create mode 100644 tests/fute/pieCharts/main.cpp create mode 100644 tests/fute/pieCharts/pieCharts.pro create mode 100644 tests/fute/pieCharts/pieCharts_test.cpp create mode 100644 tests/fute/pieCharts/pieCharts_test.h create mode 100644 tests/fute/timeCharts/main.cpp create mode 100644 tests/fute/timeCharts/timeCharts.pro create mode 100644 tests/fute/timeCharts/timeCharts_test.cpp create mode 100644 tests/fute/timeCharts/timeCharts_test.h create mode 100644 tests/mocks/CardPack_mock.cpp create mode 100644 tests/mocks/CardPack_mock.h create mode 100644 tests/mocks/Dictionary_mock.cpp create mode 100644 tests/mocks/Dictionary_mock.h create mode 100644 tests/mocks/RandomGenerator_mock.h create mode 100644 tests/mocks/TimeProvider_mock.cpp create mode 100644 tests/unit/Card/Card_GenerateAnswers_test.cpp create mode 100644 tests/unit/Card/Card_GenerateAnswers_test.h create mode 100644 tests/unit/Card/Card_test.cpp create mode 100644 tests/unit/Card/Card_test.h create mode 100644 tests/unit/Card/Card_test_QuestionAnswer.cpp create mode 100644 tests/unit/Card/card.pri create mode 100644 tests/unit/CardPack/CardPack_GenerateCards_test.cpp create mode 100644 tests/unit/CardPack/CardPack_GenerateCards_test.h create mode 100644 tests/unit/CardPack/CardPack_test.cpp create mode 100644 tests/unit/CardPack/CardPack_test.h create mode 100644 tests/unit/CardPack/cPack.pri create mode 100644 tests/unit/CardSideView/CardSideView_test.cpp create mode 100644 tests/unit/CardSideView/CardSideView_test.h create mode 100644 tests/unit/CardSideView/csView.pri create mode 100644 tests/unit/RandomGenerator/RandomGenerator_test.cpp create mode 100644 tests/unit/RandomGenerator/rndGen.pri create mode 100644 tests/unit/Settings/FieldStyleFactory_test.cpp create mode 100644 tests/unit/Settings/FieldStyleFactory_test.h create mode 100644 tests/unit/Settings/StudySettings_test.cpp create mode 100644 tests/unit/Settings/StudySettings_test.h create mode 100644 tests/unit/Settings/TestSettings.cpp create mode 100644 tests/unit/Settings/TestSettings.h create mode 100644 tests/unit/Settings/set.pri create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.cpp create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_pickCard_test.h create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_schedule_test.cpp create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_schedule_test.h create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.cpp create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_showGrades_test.h create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_test.cpp create mode 100644 tests/unit/SpacedRepetitionModel/SRModel_test.h create mode 100644 tests/unit/SpacedRepetitionModel/srModel.pri create mode 100644 tests/unit/cards.pri create mode 100644 tests/unit/common.pri create mode 100644 tests/unit/main.cpp create mode 100644 tests/unit/random.pri create mode 100644 tests/unit/studySets.pri create mode 100644 tests/unit/unit_tests.pro create mode 100644 tr/freshmemory_cs.ts create mode 100644 tr/freshmemory_de.ts create mode 100644 tr/freshmemory_en.ts create mode 100644 tr/freshmemory_es.ts create mode 100644 tr/freshmemory_fi.ts create mode 100644 tr/freshmemory_fr.ts create mode 100644 tr/freshmemory_ru.ts create mode 100644 tr/freshmemory_uk.ts create mode 100644 userdocs/_static/default.css create mode 100644 userdocs/_static/icon-note.png create mode 100644 userdocs/_static/icon-tip.png create mode 100644 userdocs/_templates/layout.html create mode 100644 userdocs/_templates/version.html create mode 100644 userdocs/about.md create mode 100644 userdocs/activation.rst create mode 100644 userdocs/browsing-cards.rst create mode 100644 userdocs/cards-generation.rst create mode 100644 userdocs/conf.py create mode 100644 userdocs/diagrams/cards_generation.dia create mode 100644 userdocs/diagrams/records.png create mode 100644 userdocs/dictionary-and-study.rst create mode 100644 userdocs/dictionary-options.rst create mode 100644 userdocs/icons/1downarrow.png create mode 100644 userdocs/icons/1uparrow.png create mode 100644 userdocs/icons/add.png create mode 100644 userdocs/icons/delete.png create mode 100644 userdocs/icons/down.png create mode 100644 userdocs/icons/filenew.png create mode 100644 userdocs/icons/filesave.png create mode 100644 userdocs/icons/passes.png create mode 100644 userdocs/icons/pencil.png create mode 100644 userdocs/icons/red-cross.png create mode 100644 userdocs/icons/up.png create mode 100755 userdocs/images/activation/about_activated.png create mode 100755 userdocs/images/activation/about_trial.png create mode 100755 userdocs/images/activation/activation_dialog.png create mode 100755 userdocs/images/activation/activation_dialog_activated.png create mode 100755 userdocs/images/activation/activation_dialog_trial.png create mode 100755 userdocs/images/activation/trial_reminder.png create mode 100644 userdocs/images/adding_image.png create mode 100644 userdocs/images/browsing.png create mode 100644 userdocs/images/browsing2.png create mode 100644 userdocs/images/cards_generation.png create mode 100644 userdocs/images/settings/field_options.png create mode 100644 userdocs/images/settings/field_style_options.png create mode 100644 userdocs/images/settings/keyword.png create mode 100644 userdocs/images/settings/pack_options.png create mode 100755 userdocs/images/settings/study_settings.png create mode 100644 userdocs/images/spacedrep/edit_card.png create mode 100644 userdocs/images/spacedrep/new_card.png create mode 100755 userdocs/images/spacedrep/progress_tooltip.png create mode 100644 userdocs/images/spacedrep/settings_exact_answer.png create mode 100644 userdocs/images/spacedrep/spacedrep.png create mode 100644 userdocs/images/spacedrep/spacedrep_exact_answer.png create mode 100644 userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.png create mode 100644 userdocs/images/spacedrep/spacedrep_hidden_answer.png create mode 100644 userdocs/images/spacedrep/study_progress.png create mode 100644 userdocs/images/ss-new_dictionary.png create mode 100644 userdocs/images/ss-word_drill_back_enabled.png create mode 100644 userdocs/images/ss-word_drill_back_pressed.png create mode 100644 userdocs/images/ss-word_drill_history.png create mode 100644 userdocs/images/ss-word_drill_second_cycle.png create mode 100644 userdocs/images/stats/chart_tooltip.png create mode 100644 userdocs/images/stats/stats_progress.png create mode 100644 userdocs/images/stats/stats_scheduled.png create mode 100644 userdocs/images/stats/stats_studied.png create mode 100644 userdocs/images/welcome_panel.png create mode 100644 userdocs/images/word_drill.png create mode 100644 userdocs/index.rst create mode 100644 userdocs/introduction.rst create mode 100644 userdocs/statistics.rst create mode 100644 userdocs/study-settings.rst create mode 100644 userdocs/studying-cards.rst create mode 100644 version.txt 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 + + 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. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + 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. ) + - 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\\Application Data\freshmemory\dictionaries + - Windows Vista/7: C:\Users\\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: + - # In the same directory as the dictionary + - # In // + - 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 @@ + + + images/freshmemory.png + images/filenew.png + images/fileopen.png + images/filesave.png + images/filesaveas.png + images/remove.png + images/exit.png + images/editcut.png + images/editcopy.png + images/editpaste.png + images/add.png + images/delete.png + images/find.png + images/next.png + images/flashcards-24.png + images/back.png + images/forward.png + images/up.png + images/down.png + images/font-style.png + images/info.png + images/openbook-24.png + images/1leftarrow.png + images/1rightarrow.png + images/1uparrow.png + images/1downarrow.png + images/warning.png + images/gray-cross.png + images/fields.png + images/spaced-rep.png + images/word-drill.png + images/dic-options.png + images/study-settings.png + images/passes.png + images/continue-search.png + images/Aa.png + images/RX.png + images/whole-words.png + images/selection.png + images/pencil.png + images/red-cross.png + images/broken-image.png + images/add-image.png + images/statistics.png + images/language.png + images/question.png + images/green-tick.png + images/green-triangle-up.png + images/blue-triangle-down.png + images/red-stop.png + images/new-topright.png + images/chart-past.png + images/chart-future.png + images/pie-chart-3d.png + images/gplv3-88x31.png + + 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 Binary files /dev/null and b/doc/chart.xcf 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": +=============================================================== + + Russian + Finnish + Example + +=============================================================== + + + +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. + +=============================================================== + + + + + Finnish + Russian + Example + English + Transcription + + + + Finnish + Russian + Example + + + Russian + Finnish + Example + + + + + perehtyä + ознакомиться + + + uhkaus + угроза + suuri uhkaus + + + усваивать + + + harrastaa + заниматься + + + alkuperäinen + начальный, первоначальный + luen [alkuperäisen] kirjan + + + lunastaa + выкупать + + + + + +Version 0.4 +=============================================================== + + + + + Finnish + Russian + Example + English + + + + perehtyä + ознакомиться + + + uhkaus + угроза + suuri uhkaus + + + усваивать + + + harrastaa + заниматься + + + alkuperäinen + начальный, первоначальный + luen [alkuperäisen] kirjan + + + выкупать + + + +=============================================================== + +Version 0.3 +=============================================================== + + + + + perehtyä + ознакомиться + + + uhkaus + угроза + suuri uhkaus + + + усваивать + + + #harrastaa + заниматься + + + alkuperäinen + начальный, первоначальный + luen %alkuperäisen kirjan + + + lunastaa + выкупать + + 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 +=========================== + + + + + + Rails + + + + + +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 Binary files /dev/null and b/doc/pie-chart.xcf 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: +* Removed tag (in the end) +* Grades changed: ("2" removed) + - 0 -> 1 + - 1 -> 2 + - 2 -> 3 +* Added level param: + Add to old tags using interval: + - i=0, c=7 -> i=, l=2 (short learning) + - i=0, c=15 -> i=, 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 **** +================================================================================= + + + + + + + + + + + + + + + + + + + + + + + + +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 in the beginning of 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: + - +* If the card-based interval is after all cards, it is == -1. +* Attribute order changed: + - + +*** dictionary.fms **** +================================================================================= + + + + + + + + + + + + + + + + + + + + + + + + + +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 **** +================================================================================= + + + + + + + + + + + + + + + + + + + + + +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 **** +================================================================================= + + + + + + + + + + + + + + + + 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 @@ + + + + + Country + Capital + Flag + + + + Country + Capital + + + Capital + Country + + + Country + Flag + + + Flag + Country + + + + + France + Paris + <img src="%%/France.png"> + + + Netherlands + Amsterdam + <img src="%%/Netherlands.png"> + + + Ukraine + Kyiv + <img src="%%/Ukraine.png"> + + + Sweden + Stockholm + <img src="%%/Sweden.png"> + + + Norway + Oslo + <img src="%%/Norway.png"> + + + Denmark + Copenhagen + <img src="%%/Denmark.png"> + + + Finland + Helsinki sdfa + <img src="%%/Finland.png"> + + + Iceland + Reykjavik + <img src="%%/Iceland.png"> + + + Great Britain + London + <img src="%%/Great Britain.png"> + + + Spain + Madrid + Computer <img src="%%/Spain.png"> Monitor hello <img src="%%/Croatia.png"> After text + + + Italy + Rome + Hello computer <img src="%%/Italy.png"> Some text here + + + Germany + Berlin + <img src="%%/Germany.png"> + + + Poland + Warsaw + <img src="%%/Poland.png"> + + + Andorra + Andorra la Vella + <img src="%%/Andorra.png"> + + + Greece + Athens + <img src="%%/Greece.png"> + + + Serbia + Belgrade + <img src="%%/Serbia.png"> + + + Russia + Moscow + <img src="%%/Russia.png"> + + + Germany + Berlin + <img src="%%/Germany.png"> + + + Switzerland + Bern + <img src="%%/Switzerland.png"> + + + Slovakia + Bratislava + <img src="%%/Slovakia.png"> + + + Belgium + Brussels + <img src="%%/Belgium.png"> + + + Romania + Bucharest + <img src="%%/Romania.png"> + + + Hungary + Budapest + <img src="%%/Hungary.png"> + + + Moldova + Chisinau + <img src="%%/Moldova.png"> + + + Ireland + Dublin + <img src="%%/Ireland.png"> + + + Portugal + Lisbon + <img src="%%/Portugal.png"> + + + Slovenia + Ljubljana + <img src="%%/Slovenia.png"> + + + Luxembourg + Luxembourg + <img src="%%/Luxembourg.png"> + + + Belarus + Minsk + <img src="%%/Belarus.png"> + + + Monaco + Monaco + <img src="%%/Monaco.png"> + + + Cyprus + Nicosia + <img src="%%/Cyprus.png"> + + + Greenland + Nuuk + <img src="%%/Greenland.png"> + + + Montenegro + Podgorica + <img src="%%/Montenegro.png"> + + + Czech Republic + Prague + <img src="%%/Czech Republic.png"> + + + Latvia + Riga + <img src="%%/Latvia.png"> + + + San Marino + San Marino + <img src="%%/San Marino.png"> + + + Bosnia & Herzegovina + Sarajevo + <img src="%%/Bosnia & Herzegovina.png"> + + + Macedonia + Skopje + <img src="%%/Macedonia.png"> + + + Bulgaria + Sofia + <img src="%%/Bulgaria.png"> + + + Estonia + Tallinn + <img src="%%/Estonia.png"> + + + Albania + Tirana + <img src="%%/Albania.png"> + + + Liechtenstein + Vaduz + <img src="%%/Liechtenstein.png"> + + + Malta + Valletta + <img src="%%/Malta.png"> + + + Austria + Vienna + <img src="%%/Austria.png"> + + + Lithuania + Vilnius + <img src="%%/Lithuania.png"> + + + Croatia + Zagreb + <img src="%%/Croatia.png"> + + + diff --git a/examples/countries-europe-2/Albania.png b/examples/countries-europe-2/Albania.png new file mode 100644 index 0000000..66a4423 Binary files /dev/null and b/examples/countries-europe-2/Albania.png differ diff --git a/examples/countries-europe-2/Andorra.png b/examples/countries-europe-2/Andorra.png new file mode 100644 index 0000000..eb1bec6 Binary files /dev/null and b/examples/countries-europe-2/Andorra.png differ diff --git a/examples/countries-europe-2/Austria.png b/examples/countries-europe-2/Austria.png new file mode 100644 index 0000000..ef2f724 Binary files /dev/null and b/examples/countries-europe-2/Austria.png differ diff --git a/examples/countries-europe-2/Belarus.png b/examples/countries-europe-2/Belarus.png new file mode 100644 index 0000000..c68042c Binary files /dev/null and b/examples/countries-europe-2/Belarus.png differ diff --git a/examples/countries-europe-2/Belgium.png b/examples/countries-europe-2/Belgium.png new file mode 100644 index 0000000..008b412 Binary files /dev/null and b/examples/countries-europe-2/Belgium.png 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 Binary files /dev/null and b/examples/countries-europe-2/Bosnia & Herzegovina.png differ diff --git a/examples/countries-europe-2/Bulgaria.png b/examples/countries-europe-2/Bulgaria.png new file mode 100644 index 0000000..832922f Binary files /dev/null and b/examples/countries-europe-2/Bulgaria.png differ diff --git a/examples/countries-europe-2/Croatia.png b/examples/countries-europe-2/Croatia.png new file mode 100644 index 0000000..0ba5cdc Binary files /dev/null and b/examples/countries-europe-2/Croatia.png differ diff --git a/examples/countries-europe-2/Cyprus.png b/examples/countries-europe-2/Cyprus.png new file mode 100644 index 0000000..7c0059a Binary files /dev/null and b/examples/countries-europe-2/Cyprus.png 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 Binary files /dev/null and b/examples/countries-europe-2/Czech Republic.png differ diff --git a/examples/countries-europe-2/Denmark.png b/examples/countries-europe-2/Denmark.png new file mode 100644 index 0000000..c91547a Binary files /dev/null and b/examples/countries-europe-2/Denmark.png differ diff --git a/examples/countries-europe-2/Estonia.png b/examples/countries-europe-2/Estonia.png new file mode 100644 index 0000000..71f56eb Binary files /dev/null and b/examples/countries-europe-2/Estonia.png differ diff --git a/examples/countries-europe-2/Finland.png b/examples/countries-europe-2/Finland.png new file mode 100644 index 0000000..38e71c3 Binary files /dev/null and b/examples/countries-europe-2/Finland.png differ diff --git a/examples/countries-europe-2/France.png b/examples/countries-europe-2/France.png new file mode 100644 index 0000000..9b1b3a7 Binary files /dev/null and b/examples/countries-europe-2/France.png differ diff --git a/examples/countries-europe-2/Germany.png b/examples/countries-europe-2/Germany.png new file mode 100644 index 0000000..f6df975 Binary files /dev/null and b/examples/countries-europe-2/Germany.png 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 Binary files /dev/null and b/examples/countries-europe-2/Great Britain.png differ diff --git a/examples/countries-europe-2/Greece.png b/examples/countries-europe-2/Greece.png new file mode 100644 index 0000000..2df5cc5 Binary files /dev/null and b/examples/countries-europe-2/Greece.png differ diff --git a/examples/countries-europe-2/Greenland.png b/examples/countries-europe-2/Greenland.png new file mode 100644 index 0000000..5bff259 Binary files /dev/null and b/examples/countries-europe-2/Greenland.png differ diff --git a/examples/countries-europe-2/Hungary.png b/examples/countries-europe-2/Hungary.png new file mode 100644 index 0000000..09ec237 Binary files /dev/null and b/examples/countries-europe-2/Hungary.png differ diff --git a/examples/countries-europe-2/Iceland.png b/examples/countries-europe-2/Iceland.png new file mode 100644 index 0000000..811436e Binary files /dev/null and b/examples/countries-europe-2/Iceland.png differ diff --git a/examples/countries-europe-2/Ireland.png b/examples/countries-europe-2/Ireland.png new file mode 100644 index 0000000..a578e43 Binary files /dev/null and b/examples/countries-europe-2/Ireland.png differ diff --git a/examples/countries-europe-2/Italy.png b/examples/countries-europe-2/Italy.png new file mode 100644 index 0000000..8d2c599 Binary files /dev/null and b/examples/countries-europe-2/Italy.png differ diff --git a/examples/countries-europe-2/Latvia.png b/examples/countries-europe-2/Latvia.png new file mode 100644 index 0000000..6ff76d2 Binary files /dev/null and b/examples/countries-europe-2/Latvia.png differ diff --git a/examples/countries-europe-2/Liechtenstein.png b/examples/countries-europe-2/Liechtenstein.png new file mode 100644 index 0000000..2e5c17a Binary files /dev/null and b/examples/countries-europe-2/Liechtenstein.png differ diff --git a/examples/countries-europe-2/Lithuania.png b/examples/countries-europe-2/Lithuania.png new file mode 100644 index 0000000..17b9018 Binary files /dev/null and b/examples/countries-europe-2/Lithuania.png differ diff --git a/examples/countries-europe-2/Luxembourg.png b/examples/countries-europe-2/Luxembourg.png new file mode 100644 index 0000000..fa6c564 Binary files /dev/null and b/examples/countries-europe-2/Luxembourg.png differ diff --git a/examples/countries-europe-2/Macedonia.png b/examples/countries-europe-2/Macedonia.png new file mode 100644 index 0000000..91e100a Binary files /dev/null and b/examples/countries-europe-2/Macedonia.png differ diff --git a/examples/countries-europe-2/Malta.png b/examples/countries-europe-2/Malta.png new file mode 100644 index 0000000..822af5d Binary files /dev/null and b/examples/countries-europe-2/Malta.png differ diff --git a/examples/countries-europe-2/Moldova.png b/examples/countries-europe-2/Moldova.png new file mode 100644 index 0000000..38c53c6 Binary files /dev/null and b/examples/countries-europe-2/Moldova.png differ diff --git a/examples/countries-europe-2/Monaco.png b/examples/countries-europe-2/Monaco.png new file mode 100644 index 0000000..98e7147 Binary files /dev/null and b/examples/countries-europe-2/Monaco.png differ diff --git a/examples/countries-europe-2/Montenegro.png b/examples/countries-europe-2/Montenegro.png new file mode 100644 index 0000000..e0cdc4a Binary files /dev/null and b/examples/countries-europe-2/Montenegro.png differ diff --git a/examples/countries-europe-2/Netherlands.png b/examples/countries-europe-2/Netherlands.png new file mode 100644 index 0000000..e6af8e6 Binary files /dev/null and b/examples/countries-europe-2/Netherlands.png differ diff --git a/examples/countries-europe-2/Norway.png b/examples/countries-europe-2/Norway.png new file mode 100644 index 0000000..6bbba1b Binary files /dev/null and b/examples/countries-europe-2/Norway.png differ diff --git a/examples/countries-europe-2/Poland.png b/examples/countries-europe-2/Poland.png new file mode 100644 index 0000000..b3e7cce Binary files /dev/null and b/examples/countries-europe-2/Poland.png differ diff --git a/examples/countries-europe-2/Portugal.png b/examples/countries-europe-2/Portugal.png new file mode 100644 index 0000000..b154563 Binary files /dev/null and b/examples/countries-europe-2/Portugal.png differ diff --git a/examples/countries-europe-2/Romania.png b/examples/countries-europe-2/Romania.png new file mode 100644 index 0000000..d12519e Binary files /dev/null and b/examples/countries-europe-2/Romania.png differ diff --git a/examples/countries-europe-2/Russia.png b/examples/countries-europe-2/Russia.png new file mode 100644 index 0000000..b751c25 Binary files /dev/null and b/examples/countries-europe-2/Russia.png 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 Binary files /dev/null and b/examples/countries-europe-2/San Marino.png differ diff --git a/examples/countries-europe-2/Serbia.png b/examples/countries-europe-2/Serbia.png new file mode 100644 index 0000000..ce93e2e Binary files /dev/null and b/examples/countries-europe-2/Serbia.png differ diff --git a/examples/countries-europe-2/Slovakia.png b/examples/countries-europe-2/Slovakia.png new file mode 100644 index 0000000..50691b8 Binary files /dev/null and b/examples/countries-europe-2/Slovakia.png differ diff --git a/examples/countries-europe-2/Slovenia.png b/examples/countries-europe-2/Slovenia.png new file mode 100644 index 0000000..300f313 Binary files /dev/null and b/examples/countries-europe-2/Slovenia.png differ diff --git a/examples/countries-europe-2/Spain.png b/examples/countries-europe-2/Spain.png new file mode 100644 index 0000000..c53d5ea Binary files /dev/null and b/examples/countries-europe-2/Spain.png differ diff --git a/examples/countries-europe-2/Sweden.png b/examples/countries-europe-2/Sweden.png new file mode 100644 index 0000000..61bf792 Binary files /dev/null and b/examples/countries-europe-2/Sweden.png differ diff --git a/examples/countries-europe-2/Switzerland.png b/examples/countries-europe-2/Switzerland.png new file mode 100644 index 0000000..98b0ece Binary files /dev/null and b/examples/countries-europe-2/Switzerland.png differ diff --git a/examples/countries-europe-2/Ukraine.png b/examples/countries-europe-2/Ukraine.png new file mode 100644 index 0000000..8c95088 Binary files /dev/null and b/examples/countries-europe-2/Ukraine.png 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 Binary files /dev/null and b/images/1downarrow.png differ diff --git a/images/1leftarrow.png b/images/1leftarrow.png new file mode 100644 index 0000000..a6c4668 Binary files /dev/null and b/images/1leftarrow.png differ diff --git a/images/1rightarrow.png b/images/1rightarrow.png new file mode 100644 index 0000000..8356b71 Binary files /dev/null and b/images/1rightarrow.png differ diff --git a/images/1uparrow.png b/images/1uparrow.png new file mode 100644 index 0000000..d6c2b99 Binary files /dev/null and b/images/1uparrow.png differ diff --git a/images/Aa.png b/images/Aa.png new file mode 100644 index 0000000..a37b74a Binary files /dev/null and b/images/Aa.png differ diff --git a/images/RX.png b/images/RX.png new file mode 100644 index 0000000..e53b2a9 Binary files /dev/null and b/images/RX.png differ diff --git a/images/add-image.png b/images/add-image.png new file mode 100644 index 0000000..adcd7b2 Binary files /dev/null and b/images/add-image.png differ diff --git a/images/add.png b/images/add.png new file mode 100644 index 0000000..0540a9b Binary files /dev/null and b/images/add.png differ diff --git a/images/attic/clock.png b/images/attic/clock.png new file mode 100644 index 0000000..a4794d8 Binary files /dev/null and b/images/attic/clock.png differ diff --git a/images/attic/large-arrow-left.png b/images/attic/large-arrow-left.png new file mode 100644 index 0000000..fd13531 Binary files /dev/null and b/images/attic/large-arrow-left.png differ diff --git a/images/attic/large-arrow-right.png b/images/attic/large-arrow-right.png new file mode 100644 index 0000000..4072bbd Binary files /dev/null and b/images/attic/large-arrow-right.png differ diff --git a/images/back.png b/images/back.png new file mode 100644 index 0000000..60caccf Binary files /dev/null and b/images/back.png differ diff --git a/images/blue-triangle-down.png b/images/blue-triangle-down.png new file mode 100644 index 0000000..bb48b1f Binary files /dev/null and b/images/blue-triangle-down.png differ diff --git a/images/broken-image.png b/images/broken-image.png new file mode 100644 index 0000000..2b098c3 Binary files /dev/null and b/images/broken-image.png differ diff --git a/images/chart-future.png b/images/chart-future.png new file mode 100644 index 0000000..05c1e15 Binary files /dev/null and b/images/chart-future.png differ diff --git a/images/chart-past.png b/images/chart-past.png new file mode 100644 index 0000000..7575035 Binary files /dev/null and b/images/chart-past.png differ diff --git a/images/continue-search.png b/images/continue-search.png new file mode 100644 index 0000000..a5a5e86 Binary files /dev/null and b/images/continue-search.png differ diff --git a/images/delete.png b/images/delete.png new file mode 100644 index 0000000..64089d7 Binary files /dev/null and b/images/delete.png differ diff --git a/images/dic-options.png b/images/dic-options.png new file mode 100644 index 0000000..8ae34d0 Binary files /dev/null and b/images/dic-options.png differ diff --git a/images/down.png b/images/down.png new file mode 100644 index 0000000..cd92e2e Binary files /dev/null and b/images/down.png differ diff --git a/images/editcopy.png b/images/editcopy.png new file mode 100644 index 0000000..f882aa2 Binary files /dev/null and b/images/editcopy.png differ diff --git a/images/editcut.png b/images/editcut.png new file mode 100644 index 0000000..79d2dca Binary files /dev/null and b/images/editcut.png differ diff --git a/images/editpaste.png b/images/editpaste.png new file mode 100644 index 0000000..a192060 Binary files /dev/null and b/images/editpaste.png differ diff --git a/images/exit.png b/images/exit.png new file mode 100644 index 0000000..7445887 Binary files /dev/null and b/images/exit.png differ diff --git a/images/fields.png b/images/fields.png new file mode 100644 index 0000000..a0d02f5 Binary files /dev/null and b/images/fields.png differ diff --git a/images/filenew.png b/images/filenew.png new file mode 100644 index 0000000..6e838b3 Binary files /dev/null and b/images/filenew.png differ diff --git a/images/fileopen.png b/images/fileopen.png new file mode 100644 index 0000000..503a004 Binary files /dev/null and b/images/fileopen.png differ diff --git a/images/filesave.png b/images/filesave.png new file mode 100644 index 0000000..dd00abd Binary files /dev/null and b/images/filesave.png differ diff --git a/images/filesaveas.png b/images/filesaveas.png new file mode 100644 index 0000000..61a080e Binary files /dev/null and b/images/filesaveas.png differ diff --git a/images/find.png b/images/find.png new file mode 100644 index 0000000..1933c2c Binary files /dev/null and b/images/find.png differ diff --git a/images/flashcards-24.png b/images/flashcards-24.png new file mode 100644 index 0000000..ca6fd98 Binary files /dev/null and b/images/flashcards-24.png differ diff --git a/images/font-style.png b/images/font-style.png new file mode 100644 index 0000000..6608631 Binary files /dev/null and b/images/font-style.png differ diff --git a/images/forward.png b/images/forward.png new file mode 100644 index 0000000..d3e6d1e Binary files /dev/null and b/images/forward.png differ diff --git a/images/freshmemory.png b/images/freshmemory.png new file mode 100644 index 0000000..e2f607a Binary files /dev/null and b/images/freshmemory.png 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 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/gplv3-88x31.png b/images/gplv3-88x31.png new file mode 100644 index 0000000..ba78d4c Binary files /dev/null and b/images/gplv3-88x31.png differ diff --git a/images/gray-cross.png b/images/gray-cross.png new file mode 100644 index 0000000..794cffd Binary files /dev/null and b/images/gray-cross.png differ diff --git a/images/green-tick.png b/images/green-tick.png new file mode 100644 index 0000000..0cd9009 Binary files /dev/null and b/images/green-tick.png differ diff --git a/images/green-triangle-up.png b/images/green-triangle-up.png new file mode 100644 index 0000000..d5963fc Binary files /dev/null and b/images/green-triangle-up.png differ diff --git a/images/info.png b/images/info.png new file mode 100644 index 0000000..96642db Binary files /dev/null and b/images/info.png differ diff --git a/images/language.png b/images/language.png new file mode 100644 index 0000000..d9fae8e Binary files /dev/null and b/images/language.png differ diff --git a/images/mainicon.ico b/images/mainicon.ico new file mode 100644 index 0000000..5097078 Binary files /dev/null and b/images/mainicon.ico differ diff --git a/images/new-topright.png b/images/new-topright.png new file mode 100644 index 0000000..165e998 Binary files /dev/null and b/images/new-topright.png differ diff --git a/images/next.png b/images/next.png new file mode 100644 index 0000000..09a97b9 Binary files /dev/null and b/images/next.png differ diff --git a/images/openbook-24.png b/images/openbook-24.png new file mode 100644 index 0000000..dd0986e Binary files /dev/null and b/images/openbook-24.png differ diff --git a/images/orig/Aa.xcf b/images/orig/Aa.xcf new file mode 100644 index 0000000..5685477 Binary files /dev/null and b/images/orig/Aa.xcf differ diff --git a/images/orig/RX.xcf b/images/orig/RX.xcf new file mode 100644 index 0000000..d1586b6 Binary files /dev/null and b/images/orig/RX.xcf differ diff --git a/images/orig/blue-triangle-down.xcf b/images/orig/blue-triangle-down.xcf new file mode 100644 index 0000000..876588c Binary files /dev/null and b/images/orig/blue-triangle-down.xcf differ diff --git a/images/orig/card.xcf b/images/orig/card.xcf new file mode 100644 index 0000000..8d60dd3 Binary files /dev/null and b/images/orig/card.xcf differ diff --git a/images/orig/chart-future.xcf b/images/orig/chart-future.xcf new file mode 100644 index 0000000..90e2ce0 Binary files /dev/null and b/images/orig/chart-future.xcf differ diff --git a/images/orig/chart-past.xcf b/images/orig/chart-past.xcf new file mode 100644 index 0000000..f4e803b Binary files /dev/null and b/images/orig/chart-past.xcf differ diff --git a/images/orig/green-triangle-up.xcf b/images/orig/green-triangle-up.xcf new file mode 100644 index 0000000..1da3dce Binary files /dev/null and b/images/orig/green-triangle-up.xcf differ diff --git a/images/orig/question.xcf b/images/orig/question.xcf new file mode 100644 index 0000000..5f711c2 Binary files /dev/null and b/images/orig/question.xcf differ diff --git a/images/orig/selection.xcf b/images/orig/selection.xcf new file mode 100644 index 0000000..6c7c845 Binary files /dev/null and b/images/orig/selection.xcf differ diff --git a/images/orig/spaced-rep.xcf b/images/orig/spaced-rep.xcf new file mode 100644 index 0000000..9d960b5 Binary files /dev/null and b/images/orig/spaced-rep.xcf differ diff --git a/images/orig/whole-words.xcf b/images/orig/whole-words.xcf new file mode 100644 index 0000000..5160c32 Binary files /dev/null and b/images/orig/whole-words.xcf differ diff --git a/images/orig/word-drill.xcf b/images/orig/word-drill.xcf new file mode 100644 index 0000000..5f9955c Binary files /dev/null and b/images/orig/word-drill.xcf differ diff --git a/images/passes.png b/images/passes.png new file mode 100644 index 0000000..586dfe6 Binary files /dev/null and b/images/passes.png differ diff --git a/images/pencil.png b/images/pencil.png new file mode 100644 index 0000000..82ed03a Binary files /dev/null and b/images/pencil.png differ diff --git a/images/pie-chart-3d.png b/images/pie-chart-3d.png new file mode 100644 index 0000000..13771be Binary files /dev/null and b/images/pie-chart-3d.png differ diff --git a/images/question.png b/images/question.png new file mode 100644 index 0000000..fa5c5c8 Binary files /dev/null and b/images/question.png differ diff --git a/images/red-cross.png b/images/red-cross.png new file mode 100644 index 0000000..3abef06 Binary files /dev/null and b/images/red-cross.png differ diff --git a/images/red-stop.png b/images/red-stop.png new file mode 100644 index 0000000..033fc67 Binary files /dev/null and b/images/red-stop.png differ diff --git a/images/remove.png b/images/remove.png new file mode 100644 index 0000000..c03d56b Binary files /dev/null and b/images/remove.png differ diff --git a/images/selection.png b/images/selection.png new file mode 100644 index 0000000..0ea4b97 Binary files /dev/null and b/images/selection.png differ diff --git a/images/spaced-rep.png b/images/spaced-rep.png new file mode 100644 index 0000000..32a0098 Binary files /dev/null and b/images/spaced-rep.png differ diff --git a/images/statistics.png b/images/statistics.png new file mode 100644 index 0000000..67e7818 Binary files /dev/null and b/images/statistics.png differ diff --git a/images/study-settings.png b/images/study-settings.png new file mode 100644 index 0000000..77f1cad Binary files /dev/null and b/images/study-settings.png differ diff --git a/images/up.png b/images/up.png new file mode 100644 index 0000000..a5b0944 Binary files /dev/null and b/images/up.png differ diff --git a/images/warning.png b/images/warning.png new file mode 100644 index 0000000..d83f349 Binary files /dev/null and b/images/warning.png differ diff --git a/images/whole-words.png b/images/whole-words.png new file mode 100644 index 0000000..59aeb7d Binary files /dev/null and b/images/whole-words.png differ diff --git a/images/word-drill.png b/images/word-drill.png new file mode 100644 index 0000000..4615f97 Binary files /dev/null and b/images/word-drill.png 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 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 +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 +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 @@ + + + + Freshmemory dictionary + + + + 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\\Application Data\freshmemory\dictionaries + - Windows Vista/7: C:\Users\\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. ) + - 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\\Application data\freshmemory\ + * Windows 7: + C:\Users\\AppData\Roaming\freshmemory\ + +Image paths in the dictionary records can be specified relative to the dictionary path: + - # In the same directory as the dictionary + - # In // + - 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 Binary files /dev/null and b/releases/screenshots/fm-1.0-e.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.0-f.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.1-a.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.1-b.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.1-c.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.1-d.png 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 Binary files /dev/null and b/releases/screenshots/fm-1.1-e.png 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& 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 +#include + +#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& 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 + +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 +#include + +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 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() + ": " + dataPoint.toolTipLabel + "
" + + scene->getYLabel() + ": " + QString::number(dataPoint.value) + ""; +} + +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 +#include + +#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 + +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 +#include +#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& 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 +#include + +#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& 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 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 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 + +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 +#include + +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& 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 +#include + +#include "DataPoint.h" + +class ChartView; +class PieChartScene; + +class PieChart: public QWidget +{ +public: + PieChart(); + void setDataSet(const QList& 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 +#include +#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& 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 +#include + +#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& dataSet); + void setColors(const QStringList& colors); + QList getDataSet() const {return dataSet; } + QStringList getColors() const { return colors; } + +private: + PieChart* chart; + QList 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 + +#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 + +#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 TimeChart::TimeUnits = QList() << + 1 << 7 << 30 << 92 << 183; + +TimeChart::TimeChart() +{ +} + +void TimeChart::setDates(const QList& dates, int period, + int dataDirection) +{ + this->dates = dates; + scene->setDataDirection(dataDirection); + TimeUnit timeUnit = findTimeUnit(period); + QDate thisIntervalStart = getIntervalStart(QDate::currentDate(), timeUnit); + QList 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 + +#include "Chart.h" + +class TimeChart: public Chart +{ +enum TimeUnit +{ + Day = 0, + Week, + Month, + Quarter, + HalfYear, + TimeUnitsNum +}; + +public: + TimeChart(); + void setDates(const QList& 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 TimeUnits; + static const int MaxDataPoints = 28; + static const int MaxXTicks = 12; + +private: + QList 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("" + tr("File name") + ": " + 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 +#include + +#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) + +#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 + +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 movedIndexes; + for(uint i=0; i + +#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 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 + +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(editor); + comboBox->setCurrentIndex( cbIndex ); + } + +void FieldStyleDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const + { + QComboBox* comboBox = static_cast(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 + +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 + +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 movedIndexes; + for(uint i=0; i 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 + +#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 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 +#include +#include +#include +#include +#include + +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 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 + +#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 + +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 + +#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 + +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 >(); + connect( aModel, SIGNAL(indexesDropped(QList)), + this, SLOT(selectIndexes(QList)), Qt::QueuedConnection ); + } + +void FieldsView::selectIndexes( QList 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 + +#include "FieldsListModel.h" + +class FieldsView : public QTableView +{ + Q_OBJECT +public: + FieldsView(QWidget *parent = 0); + + FieldsListModel* model() const + { return qobject_cast(QAbstractItemView::model()); } + + void startDrag(Qt::DropActions supportedActions); + void setModel(FieldsListModel *aModel); +public slots: + void selectIndexes(QList 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 + +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( 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( 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 + +#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 >(); + connect( aModel, SIGNAL(indexesDropped(QList)), + this, SLOT(selectIndexes(QList)), Qt::QueuedConnection ); + } + +void PackFieldsView::selectIndexes( QList 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 + +#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(QAbstractItemView::model()); } +public slots: + void selectIndexes(QList 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( pack ); + } + +void PacksListModel::insertPointer( int aPos, void* aData ) + { + beginInsertRows(QModelIndex(), aPos, aPos ); + m_parent->m_dict.insertPack( aPos, static_cast( 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 + +#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 +#include +#include +#include + +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(""+tr("Card packs")+""), 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(""+tr("Pack fields")+""), 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(""+tr("Unused fields")+""), 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( ""+tr("Preview")+"") ); + 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 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 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 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 +#include +#include +#include +#include +#include +#include + +#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 + +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 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( field ); + } + +void UnusedFieldsListModel::insertPointer( int aPos, void* aData ) + { + m_unusedFields.insert( aPos, static_cast( 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 + +#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 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 + +#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(""); + nameStr.replace( imageNameRx, "\\2"); + return nameStr; + } + +QStringList Card::getAnswers() + { + generateAnswers(); + return answers; + } + +QList Card::getSourceRecords() + { + if( sourceRecords.isEmpty() ) + generateAnswers(); + return sourceRecords; + } + +QMultiHash Card::getAnswerElements() +{ + QMultiHash 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& 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& 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 + +#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 getSourceRecords(); + StudyRecord getStudyRecord() const; + bool isScheduledAndReviewed() const; + +private: + void generateAnswers(); + void clearAnswers(); + QMultiHash getAnswerElements(); + void generateAnswersFromElements(const QMultiHash& answerElements); + QStringList getAnswerElementsForField(const QMultiHash& answerElements, + const QString& fieldName) const; + +private slots: + void dropAnswers(); + +signals: + void answersChanged() const; + +private: + const ICardPack* cardPack; + QString question; + QStringList answers; + QList 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 CardPack::getRecords() const + { + if(!m_dictionary) + return QList(); + return m_dictionary->getRecords(); + } + +const TreeItem* CardPack::parent() const + { + return dynamic_cast(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(this) ); + } + +int CardPack::topParentRow() const + { + return dynamic_cast(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 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 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 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 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 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 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 CardPack::getScheduledDates() const +{ + const int secsInDay = 24 * 60 * 60; + QList 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(m_dictionary), + SIGNAL(entryChanged(int,int)), this, SLOT(processEntryChangedEvent(int,int)) ); + disconnect(dynamic_cast(m_dictionary), + SIGNAL(entriesRemoved(int,int)), this, SLOT(processEntryChangedEvent(int)) ); + } + +void CardPack::enableDictRecordUpdates() + { + connect(dynamic_cast(m_dictionary), + SIGNAL(entryChanged(int,int)), SLOT(processEntryChangedEvent(int,int)) ); + connect(dynamic_cast(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 +#include + +#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 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 getFields() const { return fields; } + const Field* getQuestionField() const; + QList 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 getStudyRecords() const { return studyRecords.values(); } + QList getScheduledDates() const; + QList 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 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 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 fields; + + QStringList cardQuestions; ///< Original order of questions + QHash cards; ///< Card name -> card (own) + QString m_curCardName; + QMultiHash 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 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 DicCsvReader::readEntries( QString aCsvEntries, const CsvImportData& aImportData ) + { + initData( aImportData ); + QTextStream inStream( &aCsvEntries ); + readLines( inStream ); + if( !m_fieldNames.empty() ) + return m_entries; + else + return QList(); /* 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 +#include +#include +#include +#include + +#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 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 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 + +DicCsvWriter::DicCsvWriter( const Dictionary* aDict ): + m_dict( aDict ) + { + } + +DicCsvWriter::DicCsvWriter( const QList 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 +#include + +#include "../export-import/CsvData.h" + +class Dictionary; +class DicRecord; + +class DicCsvWriter +{ +public: + DicCsvWriter( const Dictionary* aDict ); // For writing from a dictionary + DicCsvWriter( const QList 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 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 + +class Field; +class ICardPack; + +class DicRecord: public QObject +{ +Q_OBJECT + +public: + DicRecord(); + DicRecord( const DicRecord& aOther ); + + const QHash 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 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 +#include +#include +#include +#include +#include + +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(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 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("

%1

").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("

%1

").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 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 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("(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 +#include +#include +#include +#include +#include +#include + +#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 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 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 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 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 +#include + +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 fList2; + fList2 << m_curCardPack->getQuestionField(); // First answer + for( int i=1; igetAnswerFields().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 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 +#include +#include + +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 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( "" ); + 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 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 + +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 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 + +/** + 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 +#include + +#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 getStudyRecords(QString cardId) const = 0; + virtual StudyRecord getStudyRecord(QString cardId) const = 0; + + virtual QList getRecords() const = 0; + virtual const Field* getQuestionField() const = 0; + virtual QList 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& 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("(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 + +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& records); + QList 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 records; + QList 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 + +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 + +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 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 codecNames = QTextCodec::availableCodecs(); + qSort( codecNames ); + for( int i=0; iaddItem(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 +#include + +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 CsvExportDialog::getUsedColumns() +{ + QList 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 +#include + +#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 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 +#include + +#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 +#include + +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 + +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 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 = "

" + BuildStr + "

"; + return QString("

") + Strings::tr(Strings::s_appTitle) + " " + FM_VERSION + "

" + + formattedBuildStr + + "

" + tr("Learn new things quickly and keep your memory fresh with time spaced repetition.") + "

" + + "

" + Strings::tr(Strings::s_author) + "

" + + "

fresh-memory.com

" + + "

" + + "

" + tr("License:") + " GPL 3" + + "

" + "

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

" + + boxSpace + "  "; + QString pEnd = "

"; + + 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(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("

" + 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 visibleGrades = static_cast(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 visibleGrades = static_cast(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(m_model)->isNew()); +} + +void SpacedRepetitionWindow::displayAnswer() +{ + static_cast(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(m_model)->isNew()) + { + exactAnswerLabel->hide(); + return; + } + else + exactAnswerLabel->show(); + bool isCorrect = correctAnswers.first() == exactAnswerEdit->text().trimmed(); + QString beginning(""; + QString answer = beginning + exactAnswerEdit->text() + ""; + 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(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("

") + + tr("All cards are reviewed") + + "
" + "

" + + tr("You can go to the next pack or dictionary, or open the Word drill.") + + "

"); +} + +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 +#include + +#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 + +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 +#include +#include + +#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( "" ); + 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 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 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(); // + } 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 +#include +#include + +#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 +#include + +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 +#include +#include + +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 + +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 + +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 +#include + +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 + +/** 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(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(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(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("
") + + tr("No cards available") + "
"); + break; + } +} + +void WordDrillWindow::DisplayCardNum() +{ +int curCardIndex = qobject_cast(m_model)->getCurCardNum(); +int curCardNum = curCardIndex + 1; +int historySize = qobject_cast(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 = ""; +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(m_model)->canGoBack() ); +iForwardBtn->setEnabled( qobject_cast(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 +#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 + +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 +#include + +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 + +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 + +#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 RecordsParam::fieldNames {"English", "Russian", "Finnish"}; + +RecordsParam::RecordsParam( + vector packFields, + vector > records, + vector questions, + vector > answers): + packFields(packFields), + questions(questions), + answers(answers) +{ + for(vector 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 RecordsParam::hashToStrVector(const QHash& hash, + const vector& keys) +{ + vector res; + for(unsigned i = 0; i < keys.size(); i++) + res.push_back( hash[keys[i].c_str()].toStdString() ); + return res; +} + +vector RecordsParam::recordsToStr() const +{ + vector res; + for(DicRecord* record: records) + { + vector fieldValues = hashToStrVector(record->getFields(), fieldNames); + res.push_back(string("(") + vectorToStr(fieldValues) + ")"); + } + return res; +} + +ostream& operator<<(ostream& os, const RecordsParam& param) +{ + os << "Fields(" << param.vectorToStr(param.packFields) << ") "; + os << "{" << param.vectorToStr( 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 +#include +#include +#include +#include + +class DicRecord; + +using std::vector; +using std::string; +using std::ostream; + +struct RecordsParam +{ +public: + static vector createParams(); + + template + static string vectorToStr(const vector& v); + + static vector hashToStrVector(const QHash& hash, + const vector& keys); + +public: + RecordsParam(vector packFields, vector > records, + vector questions, vector > answers); + + vector recordsToStr() const; + +public: + static const vector fieldNames; + +public: + QList records; + vector packFields; + vector questions; + vector > answers; +}; + +ostream& operator<<(ostream& os, const RecordsParam& param); + +template +string RecordsParam::vectorToStr(const vector& 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::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 +#include +#include + +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 +#include + +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 +#include + +#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 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 + +#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 + +#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 +#include + +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 +#include + +#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 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 + +#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 +#include +#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 +#include + +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 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 CardPackMock::getStudyRecords(QString cardId) const +{ + return studyRecords.values(cardId); +} + +StudyRecord CardPackMock::getStudyRecord(QString cardId) const +{ + return studyRecords.values(cardId).first(); +} + +QList CardPackMock::getRecords() const +{ + return QList(); +} + +const Field* CardPackMock::getQuestionField() const +{ + return NULL; +} + +QList CardPackMock::getAnswerFields() const +{ + return QList(); +} 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 + +#include "../../src/dictionary/ICardPack.h" + +class CardPackMock: public ICardPack +{ +public: + void addStudyRecord(const QString cardId, const StudyRecord& studyRecord); + QList getStudyRecords(QString cardId) const; + StudyRecord getStudyRecord(QString cardId) const; + + QList getRecords() const; + const Field* getQuestionField() const; + QList 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 + +#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 + +#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 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 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 +#include +#include + +#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 +{ +public: + GenerateAnswersTest(): + pack(&dict) {} + +protected: + void TearDown(); + void addFieldToPack(unsigned fieldId); + +public: + static const vector fieldNames; + +protected: + vector 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 +#include + +#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 + +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 + +#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 +{}; + +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 + +#include "../../common/printQtTypes.h" +#include "../../../src/dictionary/IDictionary.h" +#include "../../../src/dictionary/Field.h" +#include "../../../src/dictionary/DicRecord.h" + +vector GenerateCardsTest::fields; + +INSTANTIATE_TEST_CASE_P(, GenerateCardsTest, + testing::ValuesIn(RecordsParam::createParams()) ); + +vector GenerateCardsTest::getFields() +{ + static vector 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 +#include +#include +#include +#include + +#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 +{ +public: + static void TearDownTestCase(); + static vector getFields(); + +private: + static vector 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(), 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 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 +#include +#include + +#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 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 +#include +#include + +#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("First", + 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("First example

"\ + "Pervyj", + view.getFormattedText()); + } + +TEST_F(CardSideViewTest, getFormattedAnswer_1missing) + { + CardSideView view(CardSideView::AnsMode); + view.setPack(&cardPack); + QStringList answers = QStringList() << "" << "Pervyj"; + view.setQstAnsr("First", answers); + ASSERT_EQ("

Pervyj", + 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 + +#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 +#include +#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(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 +#include + +#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 +#include + +#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 +#include + +#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& expCards) +{ + vector 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 + +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& 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 expGrades = {4, 5}; + ASSERT_EQ(expGrades, model.getAvailableGrades()); + } + +TEST_F(SRModel_showGrades_Test, learning) + { + setStudy({StudyRecord::ShortLearning, 4, ss->initEasiness, ss->unknownInterval}); + QList expGrades = {1, 2, 4, 5}; + ASSERT_EQ(expGrades, model.getAvailableGrades()); + } + +TEST_F(SRModel_showGrades_Test, repeating) + { + setStudy({StudyRecord::Repeating, 4, ss->initEasiness, 2}); + QList 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 +#include +#include + +#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 +#include + +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 @@ + + + + + AboutDialog + + + About %1 + O %1 + + + + License: + Licence: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Učte se novým věcem rychle a udržte svou paměť čerstvou časově rozloženým opakováním. + + + + AppModel + + + No dictionary opened. + Není otevřen žádný slovník. + + + + + The current dictionary is empty. + Nynější slovník je prázdný. + + + + CardEditDialog + + + Go to dictionary window + Jít na okno se slovníkem + + + + Close + Zavřít + + + + Edit card: + In title of card edit view + Upravit kartu: + + + + CardPack + + + (empty pack) + (prádný balíček) + + + + CardPreview + + + Card preview + Náhled karty + + + + CsvDialog + + + Preview: + Náhled: + + + + Separators + Oddělovače + + + + Ta&b + Ta&b + + + + &Text delimiter: + Oddělovací znak &textu: + + + + CsvExportDialog + + + Export to CSV + Vyvést do CSV + + + + Write column &names + Zapsat &názvy sloupců + + + + C&haracter set: + Z&naková sada: + + + + Used &columns: + Použité &sloupce: + + + + Output + Výstup + + + + &Quote all fields + &Dát všechna pole do uvozovek + + + + Field &separator: + &Oddělovač pole: + + + + Co&mment character: + Znak pro &poznámku: + + + + Show &invisible characters + Ukázat &neviditelné znaky + + + + Cannot save to file: + %1. + Nelze uložit do souboru: +%1. + + + + CsvImportDialog + + + Import from CSV + Zavést z CSV + + + + C&haracter set: + Z&naková sada: + + + + From &line: + Od řá&dku: + + + + Number of colum&ns: + Počet sloup&ců: + + + + All + Vše + + + + &First line has field names + V &prvním řádku jsou názvy polí + + + + Input + Vstup + + + + An&y character + &Jakýkoliv znak + + + + Fields are separated by any separator character + + + + + A co&mbination of characters + &Kombinace znaků + + + + Fields are separated by a combination of separator characters, in any order + + + + + E&xact string + Přesné řetě&zec + + + + Fields are separated by the exact string of separators, in the above defined order + + + + + &Comment character: + Znak pro &poznámku: + + + + Field &separator: + &Oddělovač pole: + + + + Separation mode: + Režim oddělení: + + + + Dictionary + + + noname + Bez názvu + + + + + Question + Otázka + + + + + Answer + Odpověď + + + + + Example + Příklad + + + + Cannot open dictionary file: + Nelze otevřít soubor slovníku: + + + + Cannot open study file: + Nelze otevřít učební soubor: + + + + The file is not a dictionary file. + Soubor není slovníkovým souborem. + + + + + Unsupported format + Nepodporovaný formát + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + + + + + Old dictionary + Starý slovník + + + + The file is not a study file. + Soubor není učebním souborem. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Učební soubor používá nepodporovaný formát %1. +Nejnižší podporovaá verze je %2 + + + + DictionaryOptionsDialog + + + File name + Název souboru + + + + Dictionary options + Volby pro slovník + + + + Fields + Pole + + + + Card packs + Karetní balíčky + + + + FieldsListModel + + + Field + Pole + + + + Style + Styl + + + + new field + Nové pole + + + + FieldsPage + + + Fields + Pole + + + + Move up + Posunout nahoru + + + + Move down + Posunout dolů + + + + Add + Přidat + + + + Add new field + Přidat nové pole + + + + Remove + Odstranit + + + + Remove field(s) + Odstranit pole + + + + Rename + Přejmenovat + + + + Rename field + Přejmenovat pole + + + + Preview + Náhled + + + + FindPanel + + + Close + Zavřít + + + + Find: + Title of the find pane + Hledat: + + + + Find previous + Najít předchozí + + + + Find next + Najít další + + + + Case sensitive + Rozlišovat velká a malá písmena + + + + Whole words + Celá slova + + + + Regular expression + Regulární výraz + + + + In selection + Ve výběru + + + + String is not found + Řetězec nenalezen + + + + FontColorSettingsDialog + + + Font & color settings + Nastavení písma a barev + + + + Card background color: + Barva pozadí karty: + + + + Field styles + Styly polí + + + + Font family: + Rodina písma: + + + + Size: + Velikost: + + + + + Bold + Tučné + + + + + Italic + Kurzíva + + + + + Color: + Barva: + + + + Prefix: + Předpona: + + + + Suffix: + Přípona: + + + + Keyword style + Styl klíčového slova + + + + Style preview + Náhled stylu + + + + IStudyWindow + + + Close this pack + Zavřít tento balíček + + + + E + Shortcut for 'Edit card' button + E + + + + Edit card + Upravit kartu + + + + D + Shortcut for 'Delete card' button + D + + + + Delete card + Smazat kartu + + + + + Show answer + Ukázat odpověď + + + + Delete card? + Smazat kartu? + + + + Delete card "%1"? + Smazat kartu "%1"? + + + + LanguageMenu + + + &Language + &Jazyk + + + + System + Systému + + + + The application must be restarted to use the selected language + Aplikace musí být restartován, aby mohli používat zvolený jazyk + + + + MainWindow + + + Records: %1 + Záznamy: %1 + + + + Open dictionary + Otevřít slovník + + + + Save dictionary as ... + Uložit slovník jako... + + + + Import CSV file + Zavést soubor CSV + + + + Export to CSV file + Vyvést jako soubor CSV + + + + Cannot save dictionary: + Nelze uložit slovník: + + + + Cannot save study file: + Nelze uložit učební soubor: + + + + Save dictionary? + Uložit slovník? + + + + Dictionary %1 was modified. +Save changes? + Slovník %1 byl změněn. +Uložit změny? + + + + &New + &Nový + + + + Ctrl+N + Ctrl+N + + + + &Open ... + &Otevřít... + + + + Ctrl+O + Ctrl+O + + + + &Save + &Uložit + + + + Ctrl+S + Ctrl+S + + + + Save &as ... + Uložit &jako... + + + + Save &copy ... + Uložit &kopii... + + + + &Import from CSV ... + &Zavést z CSV... + + + + &Export to CSV ... + &Vyvést do CSV... + + + + &Close dictionary + &Zavřít slovník + + + + Ctrl+W + Ctrl+W + + + + &Quit + &Ukončit + + + + Ctrl+Q + Ctrl+Q + + + + &Copy + &Kopírovat + + + + Ctrl+C + Ctrl+C + + + + Cu&t + Vyj&mout + + + + Ctrl+X + Ctrl+X + + + + &Paste + &Vložit + + + + Ctrl+V + Ctrl+V + + + + S&tatistics + S&tatistika + + + + &Font and color settings + Nastavení &písma a barev + + + + Help + Nápověda + + + + About + O aplikaci + + + + The study cannot be started. + First part of error message + Učení nelze začít. + + + + Ctrl+I + Ctrl+I + + + + &Find... + &Hledat... + + + + Ctrl+F + Ctrl+F + + + + Find &again + Hledat &znovu + + + + &Word drill + Prohlížení &slov + + + + Dictionaries + Filter name in dialog + Slovníky + + + + Add image + Přidat obrázek + + + + Images + Filter name in dialog + Obrázky + + + + All files + Všechny soubory + + + + &Spaced repetition + &Rozložené opakování + + + + &Dictionary options + Volby pro &slovník + + + + &Study settings + Nastavení &učení + + + + &File + &Soubor + + + + &Recent files + &Nedávné soubory + + + + &Edit + Úp&ravy + + + + Online dictionaries + + + + + Ctrl+Z + Ctrl+Z + + + + Ctrl+Y + Ctrl+Y + + + + &Add image + Přidat &obrázek + + + + Ctrl+G + + + + + &Insert record + &Vložit záznam + + + + &Remove record + &Odstranit záznam + + + + &View + &Pohled + + + + &Tools + Ná&stroje + + + + &Options + &Volby + + + + &Help + &Nápověda + + + + Main + Hlavní + + + + Card packs + Balíčky karet + + + + The pasted records contain %n new field(s) + + Vložené záznamy obsahují %n nové pole + Vložené záznamy obsahují %n nová pole + Vložené záznamy obsahují %n nových polí + + + + + Do you want to add new fields to this dictionary? + Chcete přidat nová pole do tohoto slovníku? + + + + Add new fields + Přidat nová pole + + + + Paste only existing fields + Vložit pouze existující pole + + + + Website: + Stránky: + + + + Usage: + Použití: + + + + FILE is a dictionary filename to load. + SOUBOR je souborový název slovníku k nahrání. + + + + Options: + Volby: + + + + Display this help and exit + Zobrazit tuto nápovědu a ukončit + + + + Output version information and exit + Vypsat údaje o verzi a ukončit + + + + PacksPage + + + Add + Přidat + + + + Remove + Odstranit + + + + Move pack up + Posunout balíček nahoru + + + + Move pack down + Posunout balíček dolů + + + + Card packs + Balíčky karet + + + + Move field up + Posunout pole nahoru + + + + Move field down + Posunout pole dolů + + + + Pack fields + Pole v balíčcích + + + + Remove field from pack + Odstranit pole z balíčku + + + + Add field to pack + Přidat pole do balíčku + + + + Uses exact answer + + + + + Unused fields + Nepoužitá pole + + + + Preview + Náhled + + + + PacksTreeModel + + + Card pack + Balíček karet + + + + Sched + Rozvrh + + + + New + Nový + + + + ProgressPage + + + Studied + Učeno + + + + Scheduled for today + + + + + New + Nový + + + + Total: %1 + Celkem: %1 + + + + Study progress + + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Vložit %n záznam + Vložit %n záznamy + Vložit %n záznamů + + + + + Remove %n record(s) + Undo action of removing records + + Odstranit %n záznam + Odstranit %n záznamy + Odstranit %n záznamů + + + + + Edit "%1" + Undo action of editing a record + Upravit "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Vložit %n záznam + Vložit %n záznamy + Vložit %n záznamů + + + + + ScheduledPage + + + Scheduled cards + Zařazené karty + + + + SpacedRepetitionWindow + + + Spaced repetition + Rozložené opakování + + + + Today learned new cards + + + + + Scheduled learning reviews: +new cards must be repeated today to learn + + + + + Time left to the next learning review + + + + + Scheduled cards for today + + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + + + + + Reviewed cards + + + + + Learning reviews + + + + + Scheduled cards + Zařazené karty + + + + New cards for today + + + + + Progress of reviews scheduled for today: + + + + + Unknown + + + + + Completely forgotten card, couldn't recall the answer. + + + + + Incorrect + + + + + The answer is incorrect. + + + + + Difficult + + + + + It's difficult to recall the answer. The last interval was too long. + + + + + + Good + + + + + The answer is recalled in couple of seconds. The last interval was good enough. + + + + + Easy + + + + + The card is too easy, and recalled without any effort. The last interval was too short. + + + + + OK + + + + + (%1 min) + + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Všechny karty jsou zopakovány + + + + You can go to the next pack or dictionary, or open the Word drill. + Můžete jít na další balíček nebo slovník, nebo otevřít Prohlížení slov. + + + + StatisticsView + + + Statistics + Statistika + + + + Card pack: + Balíček karet: + + + + Period: + Perioda: + + + + + + %n week(s) + + %n týden + %n týdny + %n týdnů + + + + + + + + %n month(s) + + %n měsíc + %n měsíce + %n měsíců + + + + + + + %n year(s) + + %n rok + %n roky + %n roků + + + + + All time + Celou dobu + + + + Strings + + + Author: Mykhaylo Kopytonenko + Autor: Mykhaylo Kopytonenko + + + + Build + Sestavení + + + + Fresh Memory + Fresh Memory + + + + Error + Chyba + + + + StudiedPage + + + Studied cards + Učené karty + + + + StudySettingsDialog + + + Add new cards in random order + + + + + Day starts at, o'clock: + + + + + Share of new cards: + Podíl nových karet: + + + + Repetition interval randomness: + + + + + Day reviews limit: + + + + + Don't add new cards after scheduled cards threshold: + + + + + Limits + + + + + Day limit of new cards: + Denní nejvyšší počet nových karet: + + + + Study settings + Nastavení učení + + + + StylePreviewModel + + + keyword + Klíčové slovo + + + + TimeChartPage + + + Date + Datum + + + + Cards + Karty + + + + Total: %1 + Celkem: %1 + + + + WelcomeScreen + + + Create new dictionary + Vytvořit nový slovník + + + + Open existing dictionary + Otevřít existující slovník + + + + Open online dictionaries + + + + + Import from CSV file + Zavést z CSV souboru + + + + Recent dictionaries + Nedávné slovníky + + + + WordDrillWindow + + + Word drill + Prohlížení slov + + + + Current card / All cards + Nynější karta/Všechny karty + + + + Progress of reviewing cards + Postup opakování karet + + + + S + Shortcut for 'Show answers' checkbox + U + + + + Back + Zpět + + + + Go back in history + Jít zpět v historii + + + + Forward + Vpřed + + + + Go forward in history + Jít vpřed v historii + + + + Show next card (Enter) + Ukázat další kartu (Enter) + + + + Next + Další + + + + Show answers + Ukázat odpovědi + + + + No cards available + Nejsou dostupné žádné karty + + + 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 @@ + + + + + AboutDialog + + + About %1 + Über %1 + + + + License: + Lizenz: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Lernen Sie neues schnell und halten Sie Ihr Gedächtnis frisch mit der Wiederholung in bestimmten Abständen. + + + + AppModel + + + No dictionary opened. + Kein Wörterbuch eröffnet. + + + + + The current dictionary is empty. + Das aktuelle Wörterbuch ist leer. + + + + CardEditDialog + + + Go to dictionary window + Zum Wörterbuch Fenster + + + + Close + Schließen + + + + Edit card: + In title of card edit view + Bearbeiten Karte: + + + + CardPack + + + (empty pack) + (leer Stapel) + + + + CardPreview + + + Card preview + Kartenvorschau + + + + CsvDialog + + + Preview: + Vorschau: + + + + Separators + Trennzeichen + + + + Ta&b + + + + + &Text delimiter: + &Texttrennzeichen: + + + + CsvExportDialog + + + Export to CSV + Export nach CSV + + + + Write column &names + &Spaltennamen Schreiben + + + + C&haracter set: + &Zeichensatz: + + + + Used &columns: + Gebrauchte &Spalten: + + + + Output + Ausgabe + + + + &Quote all fields + Alle &Felde zitieren + + + + Field &separator: + &Feldtrennzeichen: + + + + Co&mment character: + &Kommentarzeichen: + + + + Show &invisible characters + &Unsichtbare Zeichen zeigen + + + + Cannot save to file: + %1. + Kann nicht auf Datei zu speichern: %1. + + + + CsvImportDialog + + + Import from CSV + Import von CSV + + + + C&haracter set: + &Zeichensatz: + + + + From &line: + Von Zei&le: + + + + Number of colum&ns: + Anzahl der &Spalten: + + + + All + Alle + + + + &First line has field names + Es gibt &Feldnamen in der erste Zeile + + + + Input + Eingabe + + + + An&y character + &Jedes Zeichen + + + + Fields are separated by any separator character + + + + + A co&mbination of characters + Eine &Kombination von Zeichens + + + + Fields are separated by a combination of separator characters, in any order + + + + + E&xact string + &Genaue Zeichenfolge + + + + Fields are separated by the exact string of separators, in the above defined order + + + + + &Comment character: + &Kommentarzeichen: + + + + Field &separator: + &Feldtrennzeichen: + + + + Separation mode: + Trennungsmodus: + + + + Dictionary + + + noname + kein_Name + + + + + Question + Frage + + + + + Answer + Antwort + + + + + Example + Beispiel + + + + Cannot open dictionary file: + Kann nicht geöffnet werden Wörterbuchdatei: + + + + Cannot open study file: + Kann nicht geöffnet Studiedatei: + + + + The file is not a dictionary file. + Die Datei ist keine Wörterbuchdatei. + + + + + Unsupported format + Nicht unterstützten Format + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + Das Wörterbuch verwendet nicht unterstütztes Format %1. +Die minimale unterstützte Version ist %2 + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + Das Wörterbuch %1 verwendet veraltete Format %2. +Es wird in das aktuelle Format %3 umgewandelt werden. + + + + Old dictionary + Alten Wörterbuch + + + + The file is not a study file. + Die Datei ist keine Studiedatei. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Die Studie Datei verwendet nicht unterstütztes Format %1. +Die minimale unterstützte Version ist %2 + + + + DictionaryOptionsDialog + + + Dictionary options + Wörterbuch Optionen + + + + File name + Dateiname + + + + Fields + Felder + + + + Card packs + Kartenstapels + + + + FieldsListModel + + + Field + Feld + + + + Style + Stil + + + + new field + neues Feld + + + + FieldsPage + + + Fields + Felder + + + + Move up + Bewegen nach oben + + + + Move down + Bewegen nach unten + + + + Add + Hinzufügen + + + + Add new field + Neues Feld hinzufügen + + + + Remove + Löschen + + + + Remove field(s) + Felder löschen + + + + Rename + Umbenennen + + + + Rename field + Feld umbenennen + + + + Preview + Vorschau + + + + FindPanel + + + Close + Schließen + + + + Find: + Title of the find pane + Suchen: + + + + Find previous + Zurücksuchen + + + + Find next + Weitersuchen + + + + Case sensitive + + + + + Whole words + Ganze Wörter + + + + Regular expression + + + + + In selection + in Auswahl + + + + String is not found + Zeichenfolge nicht gefunden + + + + FontColorSettingsDialog + + + Font & color settings + Font & Farbeinstellungen + + + + Card background color: + Karten Hintergrundfarbe: + + + + Field styles + Feldstile + + + + Font family: + Schriftart: + + + + Size: + Größe: + + + + + Bold + Fett + + + + + Italic + Kursiv + + + + + Color: + Farbe: + + + + Prefix: + Präfix: + + + + Suffix: + Suffix: + + + + Keyword style + Schlüsselwortstil + + + + Style preview + Stilvorschau + + + + IStudyWindow + + + Close this pack + Dieses Stapel schließen + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Karte bearbeiten + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Karte löschen + + + + + Show answer + Antwort zeigen + + + + Delete card? + Löschen Karte? + + + + Delete card "%1"? + Löschen Karte "%1"? + + + + LanguageMenu + + + &Language + &Sprache + + + + System + Systems + + + + The application must be restarted to use the selected language + Die Anwendung muss neu gestartet werden, um die ausgewählte Sprache zu verwenden + + + + MainWindow + + + Records: %1 + Sätze: %1 + + + + Open dictionary + Wörterbuch öffnen + + + + Dictionaries + Filter name in dialog + Wörterbücher + + + + Save dictionary as ... + Speichern Wörterbuch unter ... + + + + Import CSV file + Import CSV-datei + + + + Export to CSV file + Export nach CSV-datei + + + + Cannot save dictionary: + Kann nicht Wörterbuch zu speichern: + + + + Cannot save study file: + Kann nicht Studiedatei zu speichern: + + + + Save dictionary? + Speichern Wörterbuch? + + + + Dictionary %1 was modified. +Save changes? + Wörterbuch %1 wurde geändert. +Änderungen speichern? + + + + Add image + Bild hinzufügen + + + + Images + Filter name in dialog + Bilder + + + + All files + Alle Dateien + + + + Online dictionaries + Online Wörterbücher + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Word drill + &Wörterbetrachtungs + + + + &Spaced repetition + + + + + &Dictionary options + &Wörterbuchoptionen + + + + &Font and color settings + &Font und Farbeinstellungen + + + + &Study settings + &Studieeinstellungen + + + + &New + &Neu + + + + Ctrl+N + + + + + &Open ... + Ö&ffnen ... + + + + Ctrl+O + + + + + &Save + &Speichern + + + + Ctrl+S + + + + + Save &as ... + Speichern &unter ... + + + + Save &copy ... + &Kopie speichern ... + + + + &Import from CSV ... + &Import von CSV ... + + + + &Export to CSV ... + &Export nach CSV ... + + + + &Close dictionary + Wörterbuch s&chließen + + + + Ctrl+W + + + + + &Quit + &Beenden + + + + Ctrl+Q + + + + + &Copy + &Kopieren + + + + Ctrl+C + + + + + Cu&t + &Ausschneiden + + + + Ctrl+X + + + + + &Paste + &Einfügen + + + + Ctrl+V + + + + + &Find... + &Suchen... + + + + Ctrl+F + + + + + Find &again + &Wieder suchen + + + + &Add image + &Bild hinzufügen + + + + Ctrl+G + + + + + &Insert record + Satz ein&fügen + + + + Ctrl+I + + + + + &Remove record + Satz &löschen + + + + S&tatistics + S&tatistik + + + + Help + Hilfe + + + + About + Info + + + + &Edit + &Bearbeiten + + + + &View + &Ansicht + + + + &Tools + &Werkzeuge + + + + &Options + &Optionen + + + + &Help + &Hilfe + + + + &File + &Datei + + + + &Recent files + Ne&ueste Dateien + + + + Main + + + + + Card packs + Kartenstapels + + + + The pasted records contain %n new field(s) + + + + + + + + Do you want to add new fields to this dictionary? + + + + + Add new fields + + + + + Paste only existing fields + + + + + The study cannot be started. + First part of error message + Die Studie kann nicht gestartet werden. + + + + Website: + + + + + Usage: + + + + + FILE is a dictionary filename to load. + + + + + Options: + + + + + Display this help and exit + + + + + Output version information and exit + + + + + PacksPage + + + Add + Hinzufügen + + + + Remove + Löschen + + + + Move pack up + Stapel bewegen nach oben + + + + Move pack down + Stapel bewegen nach unten + + + + Card packs + Kartenstapels + + + + Move field up + Feld bewegen nach oben + + + + Move field down + Stapel bewegen nach unten + + + + Pack fields + Stapelfelder + + + + Remove field from pack + Feld löschen vom Stapel + + + + Add field to pack + Feld hinzufügen zu Stapel + + + + Uses exact answer + Nutzt genaue Antwort + + + + Unused fields + Ungenutzte Felder + + + + Preview + Vorschau + + + + PacksTreeModel + + + Card pack + Kartenstapel + + + + Sched + Geplan + + + + New + Neu + + + + ProgressPage + + + Studied + Studierte + + + + Scheduled for today + Geplante für heute + + + + New + Neue + + + + Total: %1 + Total: %1 + + + + Study progress + Studienfortschritt + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + %n Satz einfügen + %n Sätze einfügen + + + + + Remove %n record(s) + Undo action of removing records + + %n Satz löschen + %n Sätze löschen + + + + + Edit "%1" + Undo action of editing a record + "%1" bearbeiten + + + + Paste %n record(s) + Undo action of pasting records + + %n Satz einfügen + %n Sätze einfügen + + + + + ScheduledPage + + + Scheduled cards + Geplante karten + + + + SpacedRepetitionWindow + + + Spaced repetition + + + + + Today learned new cards + Heute gelernt neue Karten + + + + Scheduled learning reviews: +new cards must be repeated today to learn + Geplante Lernbewertungen: +neue Karten müssen heute wiederholt werden, um zu erfahren + + + + Time left to the next learning review + + + + + Scheduled cards for today + Geplante Karten für heute + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + Neue geplanten Karten für heute: +neue Karten, die zwischen den geplanten Karten gezeigt werden + + + + Reviewed cards + Angesehen Karten + + + + Learning reviews + Lernbewertungen + + + + Scheduled cards + Geplante Karten + + + + New cards for today + Neue Karten für heute + + + + Progress of reviews scheduled for today: + Fortschritt der Bewertungen für heute: + + + + Unknown + Unbekannt + + + + Completely forgotten card, couldn't recall the answer. + Völlig vergessen Karte, konnte nicht die Antwort wieder zu verwenden. + + + + Incorrect + Falsch + + + + The answer is incorrect. + Die Antwort ist falsch. + + + + Difficult + Schwer + + + + It's difficult to recall the answer. The last interval was too long. + Es ist schwierig, die Antwort zu erinnern. Das letzte Intervall zu lang war. + + + + + Good + Gut + + + + The answer is recalled in couple of seconds. The last interval was good enough. + Die Antwort wird in wenigen Sekunden zurückgerufen. Das letzte Intervall war gut genug. + + + + Easy + Einfach + + + + The card is too easy, and recalled without any effort. The last interval was too short. + Die Karte ist zu einfach, und erinnerte daran, ohne jede Anstrengung. Das letzte Intervall zu kurz war. + + + + OK + OK + + + + (%1 min) + (%1 Min) + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Alle Karten werden angesehen + + + + You can go to the next pack or dictionary, or open the Word drill. + Sie können auf den nächst Stapel oder Wörterbuch zu gehen, oder Wörterbetrachtungs zu öffnen. + + + + StatisticsView + + + Statistics + Statistik + + + + Card pack: + Kartenstapel: + + + + Period: + Zeitraum: + + + + + + %n year(s) + + %n Jahr + %n Jahre + + + + + + + %n week(s) + + %n Woche + %n Wochen + + + + + + + + %n month(s) + + %n Monat + %n Monate + + + + + All time + Die ganze Zeit + + + + Strings + + + Build + Bau + + + + Author: Mykhaylo Kopytonenko + Verfasser: Mykhaylo Kopytonenko + + + + Fresh Memory + + + + + Error + Fehler + + + + StudiedPage + + + Studied cards + Studierte Karten + + + + StudySettingsDialog + + + Add new cards in random order + Fügen neue Karten in zufälliger Reihenfolge + + + + Day starts at, o'clock: + Tag beginnt um, Uhr: + + + + Share of new cards: + Anteil neuer Karten: + + + + Repetition interval randomness: + Wiederholungsintervall Zufälligkeit: + + + + Day reviews limit: + Tage-Grenze auf Bewertungen: + + + + Don't add new cards after scheduled cards threshold: + Nicht hinzufügen neue Karten nach dem geplanten Karten Schwelle: + + + + Limits + Grenzen + + + + Day limit of new cards: + Tage-Grenze auf neuer Karten: + + + + Study settings + Studieeinstellungen + + + + StylePreviewModel + + + keyword + schlüsselwort + + + + TimeChartPage + + + Date + Datum + + + + Cards + Karten + + + + Total: %1 + Total: %1 + + + + WelcomeScreen + + + Create new dictionary + Neues Wörterbuch erstellen + + + + Open existing dictionary + Bestehenden Wörterbuch öffnen + + + + Open online dictionaries + Online wörterbücher öffnen + + + + Import from CSV file + Import von CSV-datei + + + + Recent dictionaries + Neueste Wörterbucher + + + + WordDrillWindow + + + Word drill + Wörterbetrachtungs + + + + Current card / All cards + + + + + Progress of reviewing cards + + + + + Show answers + Antworten zeigen + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Zurück + + + + Go back in history + + + + + Forward + Vorwärts + + + + Go forward in history + + + + + Next + Weiter + + + + Show next card (Enter) + + + + + No cards available + Keine Karten + + + 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 @@ + + + + + AboutDialog + + + About %1 + + + + + License: + + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + + + + + AppModel + + + No dictionary opened. + + + + + + The current dictionary is empty. + + + + + CardEditDialog + + + Go to dictionary window + + + + + Close + + + + + Edit card: + In title of card edit view + + + + + CardPack + + + (empty pack) + + + + + CardPreview + + + Card preview + + + + + CsvDialog + + + Preview: + + + + + Separators + + + + + Ta&b + + + + + &Text delimiter: + + + + + CsvExportDialog + + + Export to CSV + + + + + Write column &names + + + + + C&haracter set: + + + + + Used &columns: + + + + + Output + + + + + &Quote all fields + + + + + Field &separator: + + + + + Co&mment character: + + + + + Show &invisible characters + + + + + Cannot save to file: + %1. + + + + + CsvImportDialog + + + Import from CSV + + + + + C&haracter set: + + + + + From &line: + + + + + Number of colum&ns: + + + + + All + + + + + &First line has field names + + + + + Input + + + + + An&y character + + + + + Fields are separated by any separator character + + + + + A co&mbination of characters + + + + + Fields are separated by a combination of separator characters, in any order + + + + + E&xact string + + + + + Fields are separated by the exact string of separators, in the above defined order + + + + + &Comment character: + + + + + Field &separator: + + + + + Separation mode: + + + + + Dictionary + + + noname + + + + + + Question + + + + + + Answer + + + + + + Example + + + + + Cannot open dictionary file: + + + + + Cannot open study file: + + + + + The file is not a dictionary file. + + + + + + Unsupported format + + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + + + + + Old dictionary + + + + + The file is not a study file. + + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + + + + + DictionaryOptionsDialog + + + Dictionary options + + + + + File name + + + + + Fields + + + + + Card packs + + + + + FieldsListModel + + + Field + + + + + Style + + + + + new field + + + + + FieldsPage + + + Fields + + + + + Move up + + + + + Move down + + + + + Add + + + + + Add new field + + + + + Remove + + + + + Remove field(s) + + + + + Rename + + + + + Rename field + + + + + Preview + + + + + FindPanel + + + Close + + + + + Find: + Title of the find pane + + + + + Find previous + + + + + Find next + + + + + Case sensitive + + + + + Whole words + + + + + Regular expression + + + + + In selection + + + + + String is not found + + + + + FontColorSettingsDialog + + + Font & color settings + + + + + Card background color: + + + + + Field styles + + + + + Font family: + + + + + Size: + + + + + + Bold + + + + + + Italic + + + + + + Color: + + + + + Prefix: + + + + + Suffix: + + + + + Keyword style + + + + + Style preview + + + + + IStudyWindow + + + Close this pack + + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + + + + + + Show answer + + + + + Delete card? + + + + + Delete card "%1"? + + + + + LanguageMenu + + + &Language + + + + + System + + + + + The application must be restarted to use the selected language + The application must be restarted to use the selected language + + + + MainWindow + + + Records: %1 + + + + + Open dictionary + + + + + Dictionaries + Filter name in dialog + + + + + Save dictionary as ... + + + + + Import CSV file + + + + + Export to CSV file + + + + + Cannot save dictionary: + + + + + Cannot save study file: + + + + + Save dictionary? + + + + + Dictionary %1 was modified. +Save changes? + + + + + Add image + + + + + Images + Filter name in dialog + + + + + All files + + + + + &New + + + + + Ctrl+N + + + + + &Open ... + + + + + Ctrl+O + + + + + &Save + + + + + Ctrl+S + + + + + Save &as ... + + + + + Save &copy ... + + + + + &Import from CSV ... + + + + + &Export to CSV ... + + + + + &Close dictionary + + + + + Ctrl+W + + + + + &Quit + + + + + Ctrl+Q + + + + + &Copy + + + + + Ctrl+C + + + + + Cu&t + + + + + Ctrl+X + + + + + &Paste + + + + + Ctrl+V + + + + + &Find... + + + + + Ctrl+F + + + + + Find &again + + + + + &Add image + + + + + Ctrl+G + + + + + &Insert record + + + + + Ctrl+I + + + + + &Remove record + + + + + &Word drill + + + + + &Spaced repetition + + + + + S&tatistics + + + + + &Dictionary options + + + + + &Font and color settings + + + + + &Study settings + + + + + Help + + + + + About + + + + + &Edit + + + + + &View + + + + + &Tools + + + + + &Options + + + + + &Help + + + + + Online dictionaries + + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &File + + + + + &Recent files + + + + + Main + + + + + Card packs + + + + + The pasted records contain %n new field(s) + + The pasted records contain %n new field + The pasted records contain %n new fields + + + + + Do you want to add new fields to this dictionary? + + + + + Add new fields + + + + + Paste only existing fields + + + + + The study cannot be started. + First part of error message + + + + + Website: + + + + + Usage: + + + + + FILE is a dictionary filename to load. + + + + + Options: + + + + + Display this help and exit + + + + + Output version information and exit + + + + + PacksPage + + + Add + + + + + Remove + + + + + Move pack up + + + + + Move pack down + + + + + Card packs + + + + + Move field up + + + + + Move field down + + + + + Pack fields + + + + + Remove field from pack + + + + + Add field to pack + + + + + Uses exact answer + + + + + Unused fields + + + + + Preview + + + + + PacksTreeModel + + + Card pack + + + + + Sched + + + + + New + + + + + ProgressPage + + + Studied + + + + + Scheduled for today + + + + + New + + + + + Total: %1 + + + + + Study progress + + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Insert %n record + Insert %n records + + + + + Remove %n record(s) + Undo action of removing records + + Remove %n record + Remove %n records + + + + + Edit "%1" + Undo action of editing a record + + + + + Paste %n record(s) + Undo action of pasting records + + Paste %n record + Paste %n records + + + + + ScheduledPage + + + Scheduled cards + + + + + SpacedRepetitionWindow + + + Spaced repetition + + + + + Today learned new cards + + + + + Scheduled learning reviews: +new cards must be repeated today to learn + + + + + Time left to the next learning review + + + + + Scheduled cards for today + + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + + + + + Reviewed cards + + + + + Learning reviews + + + + + Scheduled cards + + + + + New cards for today + + + + + Progress of reviews scheduled for today: + + + + + Unknown + + + + + Completely forgotten card, couldn't recall the answer. + + + + + Incorrect + + + + + The answer is incorrect. + + + + + Difficult + + + + + It's difficult to recall the answer. The last interval was too long. + + + + + + Good + + + + + The answer is recalled in couple of seconds. The last interval was good enough. + + + + + Easy + + + + + The card is too easy, and recalled without any effort. The last interval was too short. + + + + + OK + + + + + (%1 min) + + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + + + + + You can go to the next pack or dictionary, or open the Word drill. + + + + + StatisticsView + + + Statistics + + + + + Card pack: + + + + + Period: + + + + + + + %n week(s) + + %n week + %n weeks + + + + + + + + %n month(s) + + %n month + %n months + + + + + + + %n year(s) + + %n year + %n years + + + + + All time + + + + + Strings + + + Build + + + + + Author: Mykhaylo Kopytonenko + + + + + Fresh Memory + + + + + Error + + + + + StudiedPage + + + Studied cards + + + + + StudySettingsDialog + + + Add new cards in random order + + + + + Day starts at, o'clock: + + + + + Share of new cards: + + + + + Repetition interval randomness: + + + + + Day reviews limit: + + + + + Don't add new cards after scheduled cards threshold: + + + + + Limits + + + + + Day limit of new cards: + + + + + Study settings + + + + + StylePreviewModel + + + keyword + + + + + TimeChartPage + + + Date + + + + + Cards + + + + + Total: %1 + + + + + WelcomeScreen + + + Create new dictionary + + + + + Open existing dictionary + + + + + Open online dictionaries + + + + + Import from CSV file + + + + + Recent dictionaries + + + + + WordDrillWindow + + + Word drill + + + + + Current card / All cards + + + + + Progress of reviewing cards + + + + + Show answers + + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + + + + + Go back in history + + + + + Forward + + + + + Go forward in history + + + + + Next + + + + + Show next card (Enter) + + + + + No cards available + + + + 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 @@ + + + + + AboutDialog + + + About %1 + Acerca de %1 + + + + License: + Licencia: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Aprender cosas nuevas rápidamente y mantener su memoria fresca con la repaso espaciado. + + + + AppModel + + + No dictionary opened. + No diccionario abrió. + + + + + The current dictionary is empty. + El diccionario actual está vacía. + + + + CardEditDialog + + + Go to dictionary window + Ir a ventana del diccionario + + + + Close + Cerrar + + + + Edit card: + In title of card edit view + Editar tarjeta: + + + + CardPack + + + (empty pack) + (paquete vacío) + + + + CardPreview + + + Card preview + Previsualización de tarjeta + + + + CsvDialog + + + Preview: + Previsualización: + + + + Separators + Separadores + + + + Ta&b + + + + + &Text delimiter: + Delimitador de &texto: + + + + CsvExportDialog + + + Export to CSV + Exportar a CSV + + + + Write column &names + &Escribir los nombres de columnas + + + + C&haracter set: + C&onjunto de caracteres: + + + + Used &columns: + Colu&mnas utilizadas: + + + + Output + Salida + + + + &Quote all fields + Ci&tar todos los campos + + + + Field &separator: + Sepa&rador de campos: + + + + Co&mment character: + Carácter de co&mentario: + + + + Show &invisible characters + Mostrar caracteres &invisibles + + + + Cannot save to file: + %1. + No se puede guardar archivo: + %1. + + + + CsvImportDialog + + + Import from CSV + Importar de CSV + + + + C&haracter set: + C&onjunto de caracteres: + + + + From &line: + A partir de &línea: + + + + Number of colum&ns: + Número de &columnas: + + + + All + Todos + + + + &First line has field names + &Primera línea tiene nombres de campos + + + + Input + Entrada + + + + An&y character + Cual&quier carácter + + + + Fields are separated by any separator character + + + + + A co&mbination of characters + Una co&mbinación de caracteres + + + + Fields are separated by a combination of separator characters, in any order + + + + + E&xact string + Cadena e&xacta + + + + Fields are separated by the exact string of separators, in the above defined order + + + + + &Comment character: + Carácter de co&mentario: + + + + Field &separator: + Sepa&rador de campos: + + + + Separation mode: + Modo de separación: + + + + Dictionary + + + noname + sin_nombre + + + + + Question + Pregunta + + + + + Answer + Respuesta + + + + + Example + Ejemplo + + + + Cannot open dictionary file: + No se puede abrir archivo de diccionario: + + + + Cannot open study file: + No se puede abrir archivo de estudio: + + + + The file is not a dictionary file. + Este archivo no es un archivo de diccionario. + + + + + Unsupported format + Formato no compatible + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + El diccionario utiliza el formato no compatible %1. +La versión mínima compatible es %2 + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + El diccionario %1 utiliza formato obsoleto %2. +Se convierte en el formato actual %3. + + + + Old dictionary + Viejo diccionario + + + + The file is not a study file. + Este archivo no es un archivo de estudio. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + El archivo de estudio utiliza el formato no compatible %1. +La versión mínima compatible es %2 + + + + DictionaryOptionsDialog + + + Dictionary options + Configuración de diccionario + + + + File name + Nombre del archivo + + + + Fields + Campos + + + + Card packs + Paquetes de tarjetas + + + + FieldsListModel + + + Field + Campo + + + + Style + Estilo + + + + new field + neuvo campo + + + + FieldsPage + + + Fields + Campos + + + + Move up + Arriba + + + + Move down + Abajo + + + + Add + Añadir + + + + Add new field + Añadir nuevo campo + + + + Remove + Eliminar + + + + Remove field(s) + Eliminar campos + + + + Rename + Cambiar nombre + + + + Rename field + Cambiar nombre de campo + + + + Preview + Previsualización + + + + FindPanel + + + Close + Cerrar + + + + Find: + Title of the find pane + Buscar: + + + + Find previous + Buscar anterior + + + + Find next + Buscar siguiente + + + + Case sensitive + + + + + Whole words + Palabras completas + + + + Regular expression + + + + + In selection + En selección + + + + String is not found + Cadena no se encuentra + + + + FontColorSettingsDialog + + + Font & color settings + Configuración de fuente y color + + + + Card background color: + Color de fondo de tarjeta: + + + + Field styles + Estilos de campos + + + + Font family: + Tipo de fuente: + + + + Size: + Tamaño: + + + + + Bold + Negrita + + + + + Italic + Cursiva + + + + + Color: + Color: + + + + Prefix: + Prefijo: + + + + Suffix: + Sufijo: + + + + Keyword style + Estilo de palabra clave + + + + Style preview + Previsualización de estilo + + + + IStudyWindow + + + Close this pack + Cerrar este paquete + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Editar tarjeta + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Eliminar tarjeta + + + + + Show answer + Mostrar respuesta + + + + Delete card? + Eliminar tarjeta? + + + + Delete card "%1"? + Eliminar tarjeta "%1"? + + + + LanguageMenu + + + &Language + &Idioma + + + + System + De sistema + + + + The application must be restarted to use the selected language + La aplicación debe ser reiniciado para utilizar el idioma seleccionado + + + + MainWindow + + + Records: %1 + Registros: %1 + + + + Open dictionary + Abrir diccionario + + + + Dictionaries + Filter name in dialog + Diccionarios + + + + Save dictionary as ... + Guardar diccionario como ... + + + + Import CSV file + Importar CSV archivo + + + + Export to CSV file + Exportar a CSV archivo + + + + Cannot save dictionary: + No se puede guardar diccionario: + + + + Cannot save study file: + No se puede guardar archivo de estudio: + + + + Save dictionary? + Guardar diccionario? + + + + Dictionary %1 was modified. +Save changes? + Diccionario %1 se modificó. +Guardar los cambios? + + + + Add image + Agregar imagen + + + + Images + Filter name in dialog + Imágenes + + + + All files + Todos archivos + + + + Online dictionaries + Online diccionarios + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Word drill + Examen de &palabras + + + + &Spaced repetition + &Repaso espaciado + + + + &Dictionary options + Configuración de &diccionario + + + + &Font and color settings + Configuración de &fuente y color + + + + &Study settings + Configuración de &estudio + + + + &New + &Nuevo + + + + Ctrl+N + + + + + &Open ... + &Abrir ... + + + + Ctrl+O + + + + + &Save + &Guardar + + + + Ctrl+S + + + + + Save &as ... + Guardar &como ... + + + + Save &copy ... + Guardar &copia ... + + + + &Import from CSV ... + &Importar de CSV ... + + + + &Export to CSV ... + &Exportar a CSV ... + + + + &Close dictionary + Ce&rrar diccionario + + + + Ctrl+W + + + + + &Quit + Sa&lir + + + + Ctrl+Q + + + + + &Copy + &Copiar + + + + Ctrl+C + + + + + Cu&t + C&ortar + + + + Ctrl+X + + + + + &Paste + &Pegar + + + + Ctrl+V + + + + + &Find... + &Buscar... + + + + Ctrl+F + + + + + Find &again + Buscar s&iguiente + + + + &Add image + &Agregar imagen + + + + Ctrl+G + + + + + &Insert record + &Insertar registro + + + + Ctrl+I + + + + + &Remove record + E&liminar registro + + + + S&tatistics + Es&tadística + + + + Help + Ayuda + + + + About + Acerca de + + + + &Edit + &Edición + + + + &View + &Ver + + + + &Tools + &Herramientas + + + + &Options + &Configuración + + + + &Help + &Ayuda + + + + &File + &Archivo + + + + &Recent files + Archivos &recientes + + + + Main + + + + + Card packs + Paquetes de tarjetas + + + + The pasted records contain %n new field(s) + + + + + + + + Do you want to add new fields to this dictionary? + + + + + Add new fields + + + + + Paste only existing fields + + + + + The study cannot be started. + First part of error message + El estudio no se puede iniciar. + + + + Website: + Sitio web: + + + + Usage: + + + + + FILE is a dictionary filename to load. + + + + + Options: + + + + + Display this help and exit + + + + + Output version information and exit + + + + + PacksPage + + + Add + Añadir + + + + Remove + Eliminar + + + + Move pack up + Paquete arriba + + + + Move pack down + Paquete abajo + + + + Card packs + Paquetes de tarjetas + + + + Move field up + Campo arriba + + + + Move field down + Campo abajo + + + + Pack fields + Campos de paquete + + + + Remove field from pack + Eliminar campo de paquete + + + + Add field to pack + Añadir campo a paquete + + + + Uses exact answer + Utiliza respuesta exacta + + + + Unused fields + Campos no utilizados + + + + Preview + Previsualización + + + + PacksTreeModel + + + Card pack + Paquete de tarjetas + + + + Sched + Progr + + + + New + Nuev + + + + ProgressPage + + + Studied + Estudiadas + + + + Scheduled for today + Programadas para hoy + + + + New + Nuev + + + + Total: %1 + Total: %1 + + + + Study progress + Progreso del estudio + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Insertar %n registro + Insertar %n registros + + + + + Remove %n record(s) + Undo action of removing records + + Eliminar %n registro + Eliminar %n registros + + + + + Edit "%1" + Undo action of editing a record + Editar "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Pegar %1 registro + Pegar %1 registros + + + + + ScheduledPage + + + Scheduled cards + Tarjetas programadas + + + + SpacedRepetitionWindow + + + Spaced repetition + Repaso espaciado + + + + Today learned new cards + Hoy aprendió nuevas tarjetas + + + + Scheduled learning reviews: +new cards must be repeated today to learn + Programadas opiniones de aprendizaje: +nuevas tarjetas deben repetirse hoy para aprender + + + + Time left to the next learning review + + + + + Scheduled cards for today + Tarjetas programadas para hoy + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + Nuevas tarjetas programadas para hoy: +nuevas tarjetas que se mostrarán entre las programadas tarjetas + + + + Reviewed cards + Vistas tarjetas + + + + Learning reviews + Aprendiendas revistas + + + + Scheduled cards + Tarjetas programadas + + + + New cards for today + Nuevas tarjetas para hoy + + + + Progress of reviews scheduled for today: + El progreso de los exámenes programados para hoy: + + + + Unknown + Desconocido + + + + Completely forgotten card, couldn't recall the answer. + Completamente tarjeta olvidado, no podía recordar la respuesta. + + + + Incorrect + Incorrecto + + + + The answer is incorrect. + La respuesta es incorrecta. + + + + Difficult + Difícil + + + + It's difficult to recall the answer. The last interval was too long. + Es difícil recordar la respuesta. El último intervalo era demasiado largo. + + + + + Good + Bien + + + + The answer is recalled in couple of seconds. The last interval was good enough. + La respuesta se recordó en un par de segundos. El último intervalo era lo suficientemente bueno. + + + + Easy + Fácil + + + + The card is too easy, and recalled without any effort. The last interval was too short. + La tarjeta es demasiado fácil, y recordó sin ningún esfuerzo. El último intervalo era demasiado corto. + + + + OK + Aceptar + + + + (%1 min) + (%1 min) + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Todas las tarjetas se vistas + + + + You can go to the next pack or dictionary, or open the Word drill. + Usted puede ir a la siguiente paquete o diccionario, o abrir Examen de palabras. + + + + StatisticsView + + + Statistics + Estadística + + + + Card pack: + Paquete de tarjetas: + + + + Period: + Período: + + + + + + %n year(s) + + %n año + %n años + + + + + + + %n week(s) + + %n semana + %n semanas + + + + + + + + %n month(s) + + %n mes + %n meses + + + + + All time + Todo el tiempo + + + + Strings + + + Build + Compilación + + + + Author: Mykhaylo Kopytonenko + Autor: Mykhaylo Kopytonenko + + + + Fresh Memory + + + + + Error + Error + + + + StudiedPage + + + Studied cards + Tarjetas estudiadas + + + + StudySettingsDialog + + + Add new cards in random order + Añadir nuevas tarjetas en orden aleatorio + + + + Day starts at, o'clock: + Día comienza a las: + + + + Share of new cards: + Proporción de nuevas tarjetas: + + + + Repetition interval randomness: + Aleatoriedad de intervalo de repetición: + + + + Day reviews limit: + Límite de día de revistas: + + + + Don't add new cards after scheduled cards threshold: + No agregue nuevas tarjetas después umbral de tarjetas programadas: + + + + Limits + Limites + + + + Day limit of new cards: + Límite de día de tarjetas nuevas: + + + + Study settings + Configuración de estudio + + + + StylePreviewModel + + + keyword + palabra clave + + + + TimeChartPage + + + Date + Fecha + + + + Cards + Tarjetas + + + + Total: %1 + Total: %1 + + + + WelcomeScreen + + + Create new dictionary + Crear nuevo diccionario + + + + Open existing dictionary + Abrir diccionario existente + + + + Open online dictionaries + Abrir online diccionarios + + + + Import from CSV file + Importar de CSV archivo + + + + Recent dictionaries + Diccionarios recientes + + + + WordDrillWindow + + + Word drill + Examen de palabras + + + + Current card / All cards + + + + + Progress of reviewing cards + + + + + Show answers + Mostrar respuestas + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Atrás + + + + Go back in history + + + + + Forward + Adelante + + + + Go forward in history + + + + + Next + Sigulente + + + + Show next card (Enter) + + + + + No cards available + No hay tarjetas + + + 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 @@ + + + + + AboutDialog + + + About %1 + Tietoja sovelluksesta %1 + + + + License: + Lisenssi: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Opi uutta nopeasti ja pidä muistisi tuoreena väliaikakertauksen avulla. + + + + AppModel + + + No dictionary opened. + Ei avattu mitään sanakirjaa. + + + + + The current dictionary is empty. + Nykyinen sanakirja on tyhjä. + + + + CardEditDialog + + + Go to dictionary window + Mene sanakirjan ikkunaan + + + + Close + Sulje + + + + Edit card: + In title of card edit view + Kortin muokkaaminen: + + + + CardPack + + + (empty pack) + (tyhjä pakka) + + + + CardPreview + + + Card preview + Kortin esimäkymä + + + + CsvDialog + + + Preview: + Esinäkymä: + + + + Separators + Jakajat + + + + Ta&b + Sar&k + + + + &Text delimiter: + &Tekstin rajoitin: + + + + CsvExportDialog + + + Export to CSV + Vie CSV-tidostoon + + + + Write column &names + Kirjoita sarakkeiden &nimet + + + + C&haracter set: + &Merkistö: + + + + Used &columns: + Käytetyt &sarakkeet: + + + + Output + Ulostulo + + + + &Quote all fields + &Siteeraa kaikki kentät + + + + Field &separator: + &Kenttäjakaja: + + + + Co&mment character: + Ko&mmenttimerkki: + + + + Show &invisible characters + Näytä &näkymättömiä merkkejä + + + + Cannot save to file: + %1. + Ei voida tallentaa tiedostoon: +%1. + + + + CsvImportDialog + + + Import from CSV + Tuo CSV-tiedostosta + + + + C&haracter set: + &Merkistö: + + + + From &line: + &Riviltä: + + + + Number of colum&ns: + Sarakkeiden &määrä: + + + + All + Kaikki + + + + &First line has field names + &Ensimmäisellä rivillä on kenttänimiä + + + + Input + Sisääntulo + + + + An&y character + &Mikä tahansa merkki + + + + Fields are separated by any separator character + Kenttiä erotetaan millä tahansa merkillä + + + + A co&mbination of characters + Merkki&yhdistelmä + + + + Fields are separated by a combination of separator characters, in any order + Kenttiä erotetaan merkkiyhdistelmällä, missä tahansa järjestyksessä + + + + E&xact string + &Tarkka merkkijono + + + + Fields are separated by the exact string of separators, in the above defined order + Kenttiä erotetaan tarkalla merkkijonolla, edellä mainitussa järjestyksessä + + + + &Comment character: + Ko&mmenttimerkki: + + + + Field &separator: + &Kenttäjakaja: + + + + Separation mode: + Erottamisen tila: + + + + Dictionary + + + noname + nimetön + + + + + Question + Kysymys + + + + + Answer + Vastaus + + + + + Example + Esimerkki + + + + Cannot open dictionary file: + Ei voida avata sanakirjan tiedostoa: + + + + Cannot open study file: + Ei voida avata oppimistiedostoa: + + + + The file is not a dictionary file. + Tiedosto ei ole sanakirjan tiedosto. + + + + + Unsupported format + Tukematon formaatti + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + + + + + Old dictionary + Vanha sanakirja + + + + The file is not a study file. + Tiedosto ei ole oppimistiedosto. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Oppimistiedosto käyttää tukematonta formaattia %1. +Alin tuettu versio on %2 + + + + DictionaryOptionsDialog + + + File name + Tiedostonimi + + + + Dictionary options + Sanakirjan asetukset + + + + Fields + Kentät + + + + Card packs + Korttipakat + + + + FieldsListModel + + + Field + Kenttä + + + + Style + Tyyli + + + + new field + uusi kenttä + + + + FieldsPage + + + Fields + Kentät + + + + Move up + Siirrä ylös + + + + Move down + Siirrä alas + + + + Add + Lisää + + + + Add new field + Lisää uusi kenttä + + + + Remove + Poista + + + + Remove field(s) + Poista kenttiä + + + + Rename + Nimeä uudelleen + + + + Rename field + Nimeä kenttä uudelleen + + + + Preview + Esinäkymä + + + + FindPanel + + + Close + Sulje + + + + Find: + Title of the find pane + Hae: + + + + Find previous + Etsi edeellinen + + + + Find next + Etsi seuraava + + + + Case sensitive + Kirjainkoolla on merkitystä + + + + Whole words + Kokonaiset sanat + + + + Regular expression + Säännöllinen lauseke + + + + In selection + Valituilla + + + + String is not found + Merkkijono ei etsitty + + + + FontColorSettingsDialog + + + Font & color settings + Fontti- ja väriasetukset + + + + Card background color: + Kortin taustaväri: + + + + Field styles + Kenttätyylit + + + + Font family: + Fonttinimi: + + + + Size: + Koko: + + + + + Bold + Lihava + + + + + Italic + Kursiivi + + + + + Color: + Väri: + + + + Prefix: + Etuliite: + + + + Suffix: + Pääte: + + + + Keyword style + Avainsanan tyyli + + + + Style preview + Tyylin esinäkymä + + + + IStudyWindow + + + Close this pack + Sulje tämä pakka + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Muokkaa kortti + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Poista kortti + + + + + Show answer + Näytä vastaus + + + + Delete card? + Poista kortti? + + + + Delete card "%1"? + Poista kortti "%1"? + + + + LanguageMenu + + + &Language + &Kieli + + + + System + Järjestelmän + + + + The application must be restarted to use the selected language + Ohjelman pitää käynnistää uudelleen että se käyttäisi valittua kieltä + + + + MainWindow + + + Records: %1 + Tietueita: %1 + + + + Open dictionary + Avaa sanakirja + + + + Save dictionary as ... + Tallenna sanakirja nimellä ... + + + + Import CSV file + Tuo CSV-tiedostosta + + + + Export to CSV file + Vie CSV-tiedostoon + + + + Cannot save dictionary: + Ei voida tallentaa sanakirjaa: + + + + Cannot save study file: + Ei voida tallentaa oppimistiedostoa: + + + + Save dictionary? + Tallentaa sanakirjan? + + + + Dictionary %1 was modified. +Save changes? + Sanakirja %1 oli muutettu. +Tallentaa muutokset? + + + + &New + &Uusi + + + + Ctrl+N + + + + + &Open ... + &Avaa ... + + + + Ctrl+O + + + + + &Save + &Tallenna + + + + Ctrl+S + + + + + Save &as ... + Tallenna &nimellä ... + + + + Save &copy ... + Tallenna &kopio ... + + + + &Import from CSV ... + T&uo CSV-tiedostosta ... + + + + &Export to CSV ... + &Vie CSV-tiedostoon ... + + + + &Close dictionary + &Sulje sanakirja + + + + Ctrl+W + + + + + &Quit + &Lopeta + + + + Ctrl+Q + + + + + &Copy + &Kopioi + + + + Ctrl+C + + + + + Cu&t + &Leikkaa + + + + Ctrl+X + + + + + &Paste + Lii&tä + + + + Ctrl+V + + + + + S&tatistics + S&tatistiikka + + + + &Font and color settings + &Fontti- ja väriasetukset + + + + Help + Ohje + + + + About + Tietoja sovelluksesta + + + + The study cannot be started. + First part of error message + Oppiminen ei voida alkaa. + + + + Ctrl+I + + + + + &Find... + &Etsi ... + + + + Ctrl+F + + + + + Find &again + Etsi &uudelleen + + + + &Word drill + &Sanojen harjoittelu + + + + Dictionaries + Filter name in dialog + Sanakirjat + + + + Add image + Lisää kuva + + + + Images + Filter name in dialog + Kuvat + + + + All files + Kaikki tiedostot + + + + &Spaced repetition + &Aikavälikertaus + + + + &Dictionary options + &Sanakirjan asetukset + + + + &Study settings + &Oppimisasetukset + + + + &File + &Tiedosto + + + + &Recent files + Avaa &uusin + + + + &Edit + &Muokkaa + + + + Online dictionaries + + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Add image + Lisää &kuva + + + + Ctrl+G + + + + + &Insert record + &Lisää tietue + + + + &Remove record + &Poista tietue + + + + &View + &Näytä + + + + &Tools + &Työkalut + + + + &Options + &Valinnat + + + + &Help + &Ohje + + + + Main + Pääpalkki + + + + Card packs + Korttipakat + + + + The pasted records contain %n new field(s) + + Liitetyillä tietueilla on %n uusi kenttä + Liitetyillä tietueilla on %n uutta kenttää + + + + + Do you want to add new fields to this dictionary? + Haluatko lisätä uusia kenttiä tähän sanakirjaan? + + + + Add new fields + Lisää uusia kenttiä + + + + Paste only existing fields + Liitä vain olemassa olevia kenttiä + + + + Website: + Web-sivut: + + + + Usage: + Käyttö: + + + + FILE is a dictionary filename to load. + FILE on ladattava sanakirjan tiedostonimi. + + + + Options: + Valinnat: + + + + Display this help and exit + Näytä tämä ohje ja lopeta + + + + Output version information and exit + Tulosta version tiedot ja lopeta + + + + PacksPage + + + Add + Lisää + + + + Remove + Poista + + + + Move pack up + Siirrä pakka ylös + + + + Move pack down + Siirrä pakka alas + + + + Card packs + Korttipakat + + + + Move field up + Siirrä kenttä ylös + + + + Move field down + Siirrä kenttä alas + + + + Pack fields + Pakan kentät + + + + Remove field from pack + Poista kenttä pakasta + + + + Add field to pack + Lisää kenttä pakkaan + + + + Uses exact answer + + + + + Unused fields + Käyttämättömät kentät + + + + Preview + Esinäkymä + + + + PacksTreeModel + + + Card pack + Korttipakka + + + + Sched + Suunn + + + + New + Uudet + + + + ProgressPage + + + Studied + Opitut + + + + Scheduled for today + + + + + New + Uudet + + + + Total: %1 + Yhteensä: %1 + + + + Study progress + + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Lisää %n tietue + Lisää %n tietuetta + + + + + Remove %n record(s) + Undo action of removing records + + Poista %n tietue + Poista %n tietuetta + + + + + Edit "%1" + Undo action of editing a record + Muokkaa "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Littä %n tietue + Littä %n tietuetta + + + + + ScheduledPage + + + Scheduled cards + Suunnitelleet kortit + + + + SpacedRepetitionWindow + + + Spaced repetition + Aikavälikertaus + + + + Today learned new cards + + + + + Scheduled learning reviews: +new cards must be repeated today to learn + + + + + Time left to the next learning review + + + + + Scheduled cards for today + + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + + + + + Reviewed cards + + + + + Learning reviews + + + + + Scheduled cards + Suunnitelleet kortit + + + + New cards for today + + + + + Progress of reviews scheduled for today: + + + + + Unknown + + + + + Completely forgotten card, couldn't recall the answer. + + + + + Incorrect + + + + + The answer is incorrect. + + + + + Difficult + + + + + It's difficult to recall the answer. The last interval was too long. + + + + + + Good + + + + + The answer is recalled in couple of seconds. The last interval was good enough. + + + + + Easy + + + + + The card is too easy, and recalled without any effort. The last interval was too short. + + + + + OK + + + + + (%1 min) + + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Kaikki kortit ovat selattuja + + + + You can go to the next pack or dictionary, or open the Word drill. + Voit siirtää seuraavaan pakkaan tai sanakirjaan, taikka avata Sanojen harjoittelu. + + + + StatisticsView + + + Statistics + Statistiikka + + + + Card pack: + Korttipakka: + + + + Period: + Aikaväli: + + + + + + %n week(s) + + %n viikko + %n viikkoa + + + + + + + + %n month(s) + + %n kuukausi + %n kuukautta + + + + + + + %n year(s) + + %n vuosi + %n vuotta + + + + + All time + Koko aika + + + + Strings + + + Author: Mykhaylo Kopytonenko + Tekijä: Mykhaylo Kopytonenko + + + + Build + Muutos + + + + Fresh Memory + Fresh Memory + + + + Error + Virhe + + + + StudiedPage + + + Studied cards + Opitut kortit + + + + StudySettingsDialog + + + Add new cards in random order + + + + + Day starts at, o'clock: + + + + + Share of new cards: + Uusien korttien osuus: + + + + Repetition interval randomness: + + + + + Day reviews limit: + + + + + Don't add new cards after scheduled cards threshold: + + + + + Limits + + + + + Day limit of new cards: + Uusien korttien päiväraja: + + + + Study settings + Oppimisen asetukset + + + + StylePreviewModel + + + keyword + avainsana + + + + TimeChartPage + + + Date + Päivämäärä + + + + Cards + Kortteja + + + + Total: %1 + Yhteensä: %1 + + + + WelcomeScreen + + + Create new dictionary + Luo uusi sanakirja + + + + Open existing dictionary + Avaa olemassa oleva sanakirja + + + + Open online dictionaries + + + + + Import from CSV file + Tuo CSV-tiedostosta + + + + Recent dictionaries + Uusimmat sanakirjat + + + + WordDrillWindow + + + Word drill + Sanojen harjoittelu + + + + Current card / All cards + Nykyinen kortti / Kaikki kortit + + + + Progress of reviewing cards + Korttien selaamisen tila + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Takaisin + + + + Go back in history + Palata takaisin historiassa + + + + Forward + Eteenpäin + + + + Go forward in history + Siirrä eteenpäin historiassa + + + + Show next card (Enter) + Näytä seuraava kortti (Enter) + + + + Next + Seuraava + + + + Show answers + Näytä vastaukset + + + + No cards available + Ei ole kortteja + + + 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 @@ + + + + + AboutDialog + + + About %1 + A propos %1 + + + + License: + Licence: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Apprendre de nouvelles choses rapidement et de garder votre mémoire fraîche avec répétition espacée. + + + + AppModel + + + No dictionary opened. + Aucun dictionnaire ouvert. + + + + + The current dictionary is empty. + Le dictionnaire courant est vide. + + + + CardEditDialog + + + Go to dictionary window + Aller à fenêtre de dictionnaire + + + + Close + Fermer + + + + Edit card: + In title of card edit view + Éditer la carte: + + + + CardPack + + + (empty pack) + (paquet vide) + + + + CardPreview + + + Card preview + Prévisualisation de carte + + + + CsvDialog + + + Preview: + Prévisualisation: + + + + Separators + Séparateurs + + + + Ta&b + Ta&b + + + + &Text delimiter: + Séparateur de &texte: + + + + CsvExportDialog + + + Export to CSV + Exporter au CSV + + + + Write column &names + Écrire &noms de colonnes + + + + C&haracter set: + &Jeu de caractères: + + + + Used &columns: + &Colonnes utilisées: + + + + Output + Sortie + + + + &Quote all fields + Ci&ter tous les champs + + + + Field &separator: + &Séparateur de champs: + + + + Co&mment character: + Co&mmentaire caractère: + + + + Show &invisible characters + Afficher les caractères &invisibles + + + + Cannot save to file: + %1. + Impossible d'enregistrer fichier: + %1. + + + + CsvImportDialog + + + Import from CSV + Importer de CSV + + + + C&haracter set: + &Jeu de caractères: + + + + From &line: + De &ligne: + + + + Number of colum&ns: + &Nombre de colonnes: + + + + All + Toutes + + + + &First line has field names + &Première ligne contient des noms de champs + + + + Input + Apport + + + + An&y character + &Tout caractère + + + + Fields are separated by any separator character + + + + + A co&mbination of characters + Une co&mbinaison de caractères + + + + Fields are separated by a combination of separator characters, in any order + + + + + E&xact string + Chaîne e&xacte + + + + Fields are separated by the exact string of separators, in the above defined order + + + + + &Comment character: + Co&mmentaire caractère: + + + + Field &separator: + &Séparateur de champs: + + + + Separation mode: + Mode de séparation: + + + + Dictionary + + + noname + pas_de_nom + + + + + Question + Question + + + + + Answer + Réponse + + + + + Example + Exemple + + + + Cannot open dictionary file: + Impossible d'ouvrir le fichier dictionnaire: + + + + Cannot open study file: + Impossible d'ouvrir le fichier d'étude: + + + + The file is not a dictionary file. + Le fichier est pas un fichier dictionnaire. + + + + + Unsupported format + Format non supporté + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + Le dictionnaire utilise le format non supporté %1. +La version minimum supportée est de %2 + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + Le dictionnaire %1 utilise format obsolète %2. +Il sera converti au format actuel %3. + + + + Old dictionary + Vieux dictionnaire + + + + The file is not a study file. + Le fichier est pas un fichier d'étude. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Le fichier d'étude utilise le format non supporté %1. +La version minimum supportée est de %2 + + + + DictionaryOptionsDialog + + + Dictionary options + Paramètres de dictionnaire + + + + File name + Nom du fichier + + + + Fields + Champs + + + + Card packs + Paquets de cartes + + + + FieldsListModel + + + Field + Champ + + + + Style + Style + + + + new field + nouveau champ + + + + FieldsPage + + + Fields + Champs + + + + Move up + Vers le Haut + + + + Move down + Vers le Bas + + + + Add + Ajouter + + + + Add new field + + + + + Remove + Supprimer + + + + Remove field(s) + + + + + Rename + Renommer + + + + Rename field + + + + + Preview + Prévisualisation + + + + FindPanel + + + Close + Fermer + + + + Find: + Title of the find pane + Rechercher: + + + + Find previous + + + + + Find next + Suivant + + + + Case sensitive + + + + + Whole words + Mots complets + + + + Regular expression + + + + + In selection + En sélection + + + + String is not found + Chaîne est introuvable + + + + FontColorSettingsDialog + + + Font & color settings + Paramètres de police et couleur + + + + Card background color: + Couleur de fond de carte: + + + + Field styles + Styles de champs + + + + Font family: + Famille de police: + + + + Size: + Taille: + + + + + Bold + Gras + + + + + Italic + Italique + + + + + Color: + Couleur: + + + + Prefix: + Préfixe: + + + + Suffix: + Suffixe: + + + + Keyword style + Style mot-clé + + + + Style preview + Prévisualisation de style + + + + IStudyWindow + + + Close this pack + Fermer ce paquet + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Éditer carte + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Supprimer carte + + + + + Show answer + Afficher réponse + + + + Delete card? + Supprimer carte? + + + + Delete card "%1"? + Supprimer carte "%1"? + + + + LanguageMenu + + + &Language + &Langue + + + + System + Du système + + + + The application must be restarted to use the selected language + L'application doit être redémarré pour utiliser la langue sélectionnée + + + + MainWindow + + + Records: %1 + Enregistrements: %1 + + + + Open dictionary + Ouvrir dictionnaire + + + + Dictionaries + Filter name in dialog + Dictionnaires + + + + Save dictionary as ... + Enregistrer dictionnaire sous ... + + + + Import CSV file + Importer CSV fichier + + + + Export to CSV file + Exporter au CSV fichier + + + + Cannot save dictionary: + Impossible d'enregistrer dictionnaire: + + + + Cannot save study file: + Impossible d'enregistrer fichier d'étude: + + + + Save dictionary? + Enregistrer dictionnaire? + + + + Dictionary %1 was modified. +Save changes? + Dictionnaire %1 a été modifié. +Enregistrer les modifications? + + + + Add image + Ajouter image + + + + Images + Filter name in dialog + Images + + + + All files + Tous fichiers + + + + Online dictionaries + Online dictionnaires + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Word drill + Examen de &mots + + + + &Spaced repetition + &Répétition espacée + + + + &Dictionary options + Paramètres de &dictionnaire + + + + &Font and color settings + Paramètres de &police et couleur + + + + &Study settings + &Paramètres de étude + + + + &New + &Nouveau + + + + Ctrl+N + + + + + &Open ... + &Ouvrir ... + + + + Ctrl+O + + + + + &Save + Enre&gistrer + + + + Ctrl+S + + + + + Save &as ... + Enregistrer so&us ... + + + + Save &copy ... + Enregistrer &copie ... + + + + &Import from CSV ... + &Importer depuis CSV ... + + + + &Export to CSV ... + &Exporter au CSV ... + + + + &Close dictionary + &Fermer dictionnaire + + + + Ctrl+W + + + + + &Quit + Sor&tir + + + + Ctrl+Q + + + + + &Copy + &Copier + + + + Ctrl+C + + + + + Cu&t + Co&uper + + + + Ctrl+X + + + + + &Paste + Co&ller + + + + Ctrl+V + + + + + &Find... + &Rechercher... + + + + Ctrl+F + + + + + Find &again + Suiv&ant + + + + &Add image + &Ajouter image + + + + Ctrl+G + + + + + &Insert record + &Insérer enregistrement + + + + Ctrl+I + + + + + &Remove record + Supp&rimer enregistrement + + + + S&tatistics + S&tatistique + + + + Help + Aide + + + + About + A propos + + + + &Edit + &Édition + + + + &View + A&ffichage + + + + &Tools + Ou&tils + + + + &Options + &Paramètres + + + + &Help + &Aide + + + + &File + &Fichier + + + + &Recent files + Fichiers &récents + + + + Main + + + + + Card packs + Paquets de cartes + + + + The pasted records contain %n new field(s) + + + + + + + + Do you want to add new fields to this dictionary? + + + + + Add new fields + + + + + Paste only existing fields + + + + + The study cannot be started. + First part of error message + L'étude ne peut pas être démarré. + + + + Website: + Site web: + + + + Usage: + + + + + FILE is a dictionary filename to load. + + + + + Options: + + + + + Display this help and exit + + + + + Output version information and exit + + + + + PacksPage + + + Add + Ajouter + + + + Remove + Supprimer + + + + Move pack up + Paquet vers le haut + + + + Move pack down + Paquet vers le bas + + + + Card packs + Paquets de cartes + + + + Move field up + Champ vers le haut + + + + Move field down + Champ vers le bas + + + + Pack fields + Champs de paquet + + + + Remove field from pack + + + + + Add field to pack + + + + + Uses exact answer + + + + + Unused fields + Champs inutilisés + + + + Preview + Prévisualisation + + + + PacksTreeModel + + + Card pack + Paquet de cartes + + + + Sched + Plan + + + + New + Nouv + + + + ProgressPage + + + Studied + Étudiées + + + + Scheduled for today + Planifiées pour aujourd'hui + + + + New + Nouv + + + + Total: %1 + Total: %1 + + + + Study progress + Progrès de l'étude + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Insérer %n enregistrement + Insérer %n enregistrements + + + + + Remove %n record(s) + Undo action of removing records + + Supprimer %n enregistrement + Supprimer %n enregistrements + + + + + Edit "%1" + Undo action of editing a record + Éditer "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Coller %n enregistrement + Coller %n enregistrements + + + + + ScheduledPage + + + Scheduled cards + Cartes planifiées + + + + SpacedRepetitionWindow + + + Spaced repetition + Répétition espacée + + + + Today learned new cards + + + + + Scheduled learning reviews: +new cards must be repeated today to learn + + + + + Time left to the next learning review + + + + + Scheduled cards for today + + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + + + + + Reviewed cards + + + + + Learning reviews + + + + + Scheduled cards + Cartes planifiées + + + + New cards for today + + + + + Progress of reviews scheduled for today: + + + + + Unknown + Inconnu + + + + Completely forgotten card, couldn't recall the answer. + + + + + Incorrect + Incorrecte + + + + The answer is incorrect. + + + + + Difficult + Difficile + + + + It's difficult to recall the answer. The last interval was too long. + + + + + + Good + Bien + + + + The answer is recalled in couple of seconds. The last interval was good enough. + + + + + Easy + Facile + + + + The card is too easy, and recalled without any effort. The last interval was too short. + + + + + OK + OK + + + + (%1 min) + (%1 min) + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Toutes les cartes sont examinées + + + + You can go to the next pack or dictionary, or open the Word drill. + Vous pouvez aller à le meute paquet ou un dictionnaire, ou ouvrir l'Examen de mots. + + + + StatisticsView + + + Statistics + Statistique + + + + Card pack: + Paquet de cartes: + + + + Period: + Période: + + + + + + %n week(s) + + %n semaine + %n semaines + + + + + + + + %n month(s) + + %n mois + %n mois + + + + + + + %n year(s) + + %n an + %n années + + + + + All time + Tout le temps + + + + Strings + + + Build + Construcion + + + + Author: Mykhaylo Kopytonenko + Auteur: Mykhaylo Kopytonenko + + + + Fresh Memory + + + + + Error + Erreur + + + + StudiedPage + + + Studied cards + Сartes étudiées + + + + StudySettingsDialog + + + Add new cards in random order + Ajouter nouvelles cartes dans un ordre aléatoire + + + + Day starts at, o'clock: + Journée commence à: + + + + Share of new cards: + Partager de nouvelles cartes: + + + + Repetition interval randomness: + Aléatoire de Intervalle de répétition: + + + + Day reviews limit: + Limite de jour pour répétitions: + + + + Don't add new cards after scheduled cards threshold: + Ne pas ajouter de nouvelles cartes après seuil des cartes programmées: + + + + Limits + Limites + + + + Day limit of new cards: + Limite jour de nouvelles cartes: + + + + Study settings + Paramètres de étude + + + + StylePreviewModel + + + keyword + mot-clé + + + + TimeChartPage + + + Date + Date + + + + Cards + Cartes + + + + Total: %1 + Total: %1 + + + + WelcomeScreen + + + Create new dictionary + Créer nouveau dictionnaire + + + + Open existing dictionary + Ouvrir dictionnaire existant + + + + Open online dictionaries + Ouvrir dictionnaires online + + + + Import from CSV file + Importer de CSV fichier + + + + Recent dictionaries + Dictionnaires récents + + + + WordDrillWindow + + + Word drill + Examen de mots + + + + Current card / All cards + + + + + Progress of reviewing cards + + + + + Show answers + Afficher réponses + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Précédent + + + + Go back in history + + + + + Forward + Avant + + + + Go forward in history + + + + + Next + Suivant + + + + Show next card (Enter) + + + + + No cards available + Pas de cartes + + + 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 @@ + + + + + AboutDialog + + + About %1 + О программе %1 + + + + License: + Лицензия: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Изучайте новое быстро и поддерживайте память методом интервальных повторений. + + + + AppModel + + + No dictionary opened. + Не открыт ни один словарь. + + + + + The current dictionary is empty. + Текущий словарь пуст. + + + + CardEditDialog + + + Go to dictionary window + Перейти к словарю + + + + Close + Закрыть + + + + Edit card: + In title of card edit view + Редактирование карточки: + + + + CardPack + + + (empty pack) + (пустая колода) + + + + CardPreview + + + Card preview + Просмотр карточки + + + + CsvDialog + + + Preview: + Просмотр: + + + + Separators + Разделители + + + + Ta&b + Та&б + + + + &Text delimiter: + О&граничитель текста: + + + + CsvExportDialog + + + Export to CSV + Экспорт в CSV + + + + Write column &names + Вписать &имена колонок + + + + C&haracter set: + Кодиро&вка: + + + + Used &columns: + Используемые &колонки: + + + + Output + Вывод + + + + &Quote all fields + Заключить в к&авычки все поля + + + + Field &separator: + &Разделитель полей: + + + + Co&mment character: + Символ ко&мментария: + + + + Show &invisible characters + Показать &невидимые символы + + + + Cannot save to file: + %1. + Невозможно сохранить в файл:\n %1. + + + + CsvImportDialog + + + Import from CSV + Импорт из CSV файла + + + + C&haracter set: + Кодиро&вка: + + + + From &line: + С&о строки: + + + + Number of colum&ns: + &Количество колонок: + + + + All + Все + + + + &First line has field names + В &первой строке имена полей + + + + Input + Ввод + + + + An&y character + &Любой символ + + + + Fields are separated by any separator character + Поля разделяются любым символом-разделителем + + + + A co&mbination of characters + &Комбинация символов + + + + Fields are separated by a combination of separator characters, in any order + Поля разделяются комбинацией символов-разделителей, в любом порядке + + + + E&xact string + &Определенная строка + + + + Fields are separated by the exact string of separators, in the above defined order + Поля разделяются в точности определенной строкой из разделителей, в вышеуказанном порядке + + + + &Comment character: + Символ &комментария: + + + + Field &separator: + &Разделитель полей: + + + + Separation mode: + Режим разделения: + + + + Dictionary + + + noname + безымянный + + + + + Question + Вопрос + + + + + Answer + Ответ + + + + + Example + Пример + + + + Cannot open dictionary file: + Невозможно открыть файл словаря: + + + + Cannot open study file: + Невозможно открыть файл обучения: + + + + The file is not a dictionary file. + Файл не является словарем. + + + + + Unsupported format + Неподдерживаемый формат + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + Словарь использует неподдерживаемый формат %1. +Минимальная поддерживаемая версия - %2 + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + Словарь %1 использует устаревший формат %2. +Он будет преобразован в текущий формат %3. + + + + Old dictionary + Старый словарь + + + + The file is not a study file. + Этот файл не является файлом обучения. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Файл обучения использует неподдерживаемый формат %1.\nМинимальная поддерживаемая версия - %2 + + + + DictionaryOptionsDialog + + + File name + Имя файла + + + + Dictionary options + Параметры словаря + + + + Fields + Поля + + + + Card packs + Колоды карточек + + + + FieldsListModel + + + Field + Поле + + + + Style + Стиль + + + + new field + новое поле + + + + FieldsPage + + + Fields + Поля + + + + Move up + Передвинуть вверх + + + + Move down + Передвинуть вниз + + + + Add + Добавить + + + + Add new field + Добавить новое поле + + + + Remove + Удалить + + + + Remove field(s) + Удалить поля + + + + Rename + Переименовать + + + + Rename field + Переименовать поле + + + + Preview + Просмотр + + + + FindPanel + + + Close + Закрыть + + + + Find: + Title of the find pane + Поиск: + + + + Find previous + Найти предыдущую + + + + Find next + Найти следующую + + + + Case sensitive + Учитывать регистр символов + + + + Whole words + Слова целиком + + + + Regular expression + Регулярное выражение + + + + In selection + В выбранных + + + + String is not found + Строка не найдена + + + + FontColorSettingsDialog + + + Font & color settings + Настройки шрифта и цвета + + + + Card background color: + Цвет фона карточек: + + + + Field styles + Стили полей + + + + Font family: + Шрифт: + + + + Size: + Размер: + + + + + Bold + Жирный + + + + + Italic + Курсив + + + + + Color: + Цвет: + + + + Prefix: + Префикс: + + + + Suffix: + Суффикс: + + + + Keyword style + Стиль ключевого слова + + + + Style preview + Просмотр стилей + + + + IStudyWindow + + + Close this pack + Закрыть эту колоду + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Редактировать карточку + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Удалить карточку + + + + + Show answer + Показать ответ + + + + Delete card? + Удалить карточку? + + + + Delete card "%1"? + Удалить карточку "%1"? + + + + LanguageMenu + + + &Language + &Язык + + + + System + Системный + + + + The application must be restarted to use the selected language + Нужно перезапустить приложение, чтобы использовать выбранный язык + + + + MainWindow + + + Records: %1 + Записей: %1 + + + + Open dictionary + Открыть словарь + + + + Save dictionary as ... + Сохранить словарь как ... + + + + Import CSV file + Импорт из CSV файла + + + + Export to CSV file + Экспорт в CSV файл + + + + Save dictionary? + Сохранить словарь? + + + + Dictionary %1 was modified. +Save changes? + Словарь %1 изменен. Сохранить изменения? + + + + &New + &Новый + + + + Ctrl+N + + + + + &Close dictionary + &Закрыть словарь + + + + &Open ... + &Открыть ... + + + + Cannot save dictionary: + Невозможно сохранить словарь: + + + + Cannot save study file: + Невозможно сохранить файл обучения: + + + + Add image + Вставить картинку + + + + Images + Filter name in dialog + Картинки + + + + All files + Все файлы + + + + Ctrl+O + + + + + Online dictionaries + Онлайн словари + + + + &Save + &Сохранить + + + + Ctrl+S + + + + + Save &as ... + Сохранить &как... + + + + Save &copy ... + Сохранить копи&ю ... + + + + &Import from CSV ... + &Импорт из CSV ... + + + + &Export to CSV ... + &Экспорт в CSV ... + + + + Ctrl+W + + + + + &Quit + &Выход + + + + Ctrl+Q + + + + + &Copy + &Копировать + + + + Ctrl+C + + + + + Cu&t + Вы&резать + + + + Ctrl+X + + + + + &Paste + &Вставить из буфера + + + + Ctrl+V + + + + + S&tatistics + С&татистика + + + + &Font and color settings + Настройки &шрифта и цвета + + + + Help + Справка + + + + About + О программе + + + + The study cannot be started. + First part of error message + Невозможно начать обучение. + + + + Ctrl+I + + + + + &Find... + П&оиск... + + + + Ctrl+F + + + + + Find &again + Найти с&нова + + + + &Word drill + &Просмотр слов + + + + Dictionaries + Filter name in dialog + Словари + + + + &Spaced repetition + &Интервальное повторение + + + + &Dictionary options + Настройки &словаря + + + + &Study settings + Настройки &обучения + + + + &File + &Файл + + + + &Recent files + &Последние файлы + + + + &Edit + &Правка + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Add image + &Вставить картинку + + + + Ctrl+G + + + + + &Insert record + &Вставить запись + + + + &Remove record + &Удалить запись + + + + &View + &Вид + + + + &Tools + &Инструменты + + + + &Options + Пара&метры + + + + &Help + &Справка + + + + Main + Главная + + + + Card packs + Колоды карточек + + + + The pasted records contain %n new field(s) + + Вставляемые записи содержат %n новое поле + Вставляемые записи содержат %n новых поля + Вставляемые записи содержат %n новых полей + + + + + Do you want to add new fields to this dictionary? + Вы хотите добавить новые поля в этот словарь? + + + + Add new fields + Добавить новые поля + + + + Paste only existing fields + Вставить только существующие поля + + + + Website: + Веб-сайт: + + + + Usage: + Использование: + + + + FILE is a dictionary filename to load. + FILE - имя загружаемого файла словаря. + + + + Options: + Параметры: + + + + Display this help and exit + Показать эту справку и выйти + + + + Output version information and exit + Вывести информацию о версии и выйти + + + + PacksPage + + + Add + Добавить + + + + Remove + Удалить + + + + Move pack up + Передвинуть колоду вверх + + + + Move pack down + Передвинуть колоду вниз + + + + Card packs + Колоды карточек + + + + Move field up + Передвинуть поле вверх + + + + Move field down + Передвинуть поле вниз + + + + Pack fields + Поля колоды + + + + Remove field from pack + Удалить поле из колоды + + + + Add field to pack + Добавть поле в колоду + + + + Uses exact answer + Использует точный ответ + + + + Unused fields + Неиспользованные поля + + + + Preview + Просмотр + + + + PacksTreeModel + + + Card pack + Колода карточек + + + + Sched + Заплан + + + + New + Нов + + + + ProgressPage + + + Studied + Изученные + + + + Scheduled for today + Запланир. на сегодня + + + + New + Новые + + + + Total: %1 + Всего: %1 + + + + Study progress + Ход обучения + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Вставить %n запись + Вставить %n записи + Вставить %n записей + + + + + Remove %n record(s) + Undo action of removing records + + Удалить %n запись + Удалить %n записи + Удалить %n записей + + + + + Edit "%1" + Undo action of editing a record + Редактировать "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Вставить из буфера %n запись + Вставить из буфера %n записи + Вставить из буфера %n записей + + + + + ScheduledPage + + + Scheduled cards + Запланированные карточки + + + + SpacedRepetitionWindow + + + Spaced repetition + Интервальное повторение + + + + Today learned new cards + Сегодня изученные новые карточки + + + + Scheduled learning reviews: +new cards must be repeated today to learn + Запланированные обучающие просмотры: +новые карточки нужно повторить сегодня чтобы их выучить + + + + Time left to the next learning review + Оставшееся время до следующего обучающего просмотра + + + + Scheduled cards for today + Запланированные карточки на сегодня + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + Новые запланированные карточки на сегодня: +новые карточки, которые будут показаны между запланированными + + + + Reviewed cards + Просмотренные карточки + + + + Learning reviews + Обучающие просмотры + + + + Scheduled cards + Запланированные карточки + + + + New cards for today + Новые карточки на сегодня + + + + Progress of reviews scheduled for today: + Ход просмотра запланированных на сегодня карточек: + + + + Unknown + Не знаю + + + + Completely forgotten card, couldn't recall the answer. + Совершенно забытая карточка, не могу вспомнить ответ. + + + + Incorrect + Неправильно + + + + The answer is incorrect. + Ответ неправильный. + + + + Difficult + Трудно + + + + It's difficult to recall the answer. The last interval was too long. + Трудно вспомнить ответ. Последний интервал был слишком длинным. + + + + + Good + Хорошо + + + + The answer is recalled in couple of seconds. The last interval was good enough. + Ответ вспоминается через пару секунд. Последний интервал был подходящим. + + + + Easy + Легко + + + + The card is too easy, and recalled without any effort. The last interval was too short. + Карточка слишком легкая, и вспоминается без усилий. Последний интервал был слишком коротким. + + + + OK + OK + + + + (%1 min) + (%1 мин) + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + Достигнут дневной предел для карточек: %1 штук. +Рекомендуется закончить изучение этого словаря. + + + + All cards are reviewed + Все карточки просмотрены + + + + You can go to the next pack or dictionary, or open the Word drill. + Вы можете перейти к следующей колоде или словарю, или открыть Просмотр слов. + + + + StatisticsView + + + Statistics + Статистика + + + + Card pack: + Колода карточек: + + + + Period: + Период: + + + + + + %n year(s) + + %n год + %n года + %n лет + + + + + + + %n week(s) + + %n неделя + %n недели + %n недель + + + + + + + + %n month(s) + + %n месяц + %n месяца + %n месяцев + + + + + All time + Всё время + + + + Strings + + + Author: Mykhaylo Kopytonenko + Автор: Михаил Копитоненко + + + + Build + Сборка + + + + Fresh Memory + + + + + Error + Ошибка + + + + StudiedPage + + + Studied cards + Изученные карточки + + + + StudySettingsDialog + + + Add new cards in random order + Добавлять новые карточки в случайном порядке + + + + Day starts at, o'clock: + День начинается в, часов: + + + + Share of new cards: + Доля новых карточек: + + + + Repetition interval randomness: + Разброс интервала повторений: + + + + Day reviews limit: + Дневное ограничение для просмотров: + + + + Don't add new cards after scheduled cards threshold: + Не добавлять новых карточек после предела запланированных карточек: + + + + Limits + Ограничения + + + + Day limit of new cards: + Дневное ограничение для новых карточек: + + + + Study settings + Настройки обучения + + + + StylePreviewModel + + + keyword + ключевое слово + + + + TimeChartPage + + + Date + Дата + + + + Cards + Карточки + + + + Total: %1 + Всего: %1 + + + + WelcomeScreen + + + Create new dictionary + Создать новый словарь + + + + Open existing dictionary + Открыть существующий словарь + + + + Open online dictionaries + Открыть онлайн словари + + + + Import from CSV file + Импорт из CSV файла + + + + Recent dictionaries + Последние словари + + + + WordDrillWindow + + + Current card / All cards + Текущая карточка / Все карточки + + + + Progress of reviewing cards + Состояние просмотра карточек + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Назад + + + + Go back in history + Вернуться назад по истории + + + + Forward + Вперёд + + + + Go forward in history + Пройти вперёд по истории + + + + Show next card (Enter) + Показать следующую карточку (Enter) + + + + Next + Далее + + + + Word drill + Просмотр слов + + + + Show answers + Показывать ответы + + + + No cards available + Нет карточек + + + 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 @@ + + + + + AboutDialog + + + About %1 + Про %1 + + + + License: + Ліцензія: + + + + Learn new things quickly and keep your memory fresh with time spaced repetition. + Вивчайте нове швидко і підтримуйте свою пам’ять методом інтервальних повторень. + + + + AppModel + + + No dictionary opened. + Не відкритий жоден словник. + + + + + The current dictionary is empty. + Поточний словник пустий. + + + + CardEditDialog + + + Go to dictionary window + Перейти до словника + + + + Close + Закрити + + + + Edit card: + In title of card edit view + Редагування картки: + + + + CardPack + + + (empty pack) + (пуста колода) + + + + CardPreview + + + Card preview + Перегляд картки + + + + CsvDialog + + + Preview: + Перегляд: + + + + Separators + Роздільники + + + + Ta&b + Та&б + + + + &Text delimiter: + Обмежувач &тексту: + + + + CsvExportDialog + + + Export to CSV + Експорт в CSV + + + + Write column &names + Вписати &імена стовпчиків + + + + C&haracter set: + Коду&вання: + + + + Used &columns: + Використані &стовпчики: + + + + Output + Вивід + + + + &Quote all fields + Помістити всі поля в &лапки + + + + Field &separator: + &Роздільник полів: + + + + Co&mment character: + Символ ко&ментаря: + + + + Show &invisible characters + Показати &невидимі символи + + + + Cannot save to file: + %1. + Неможливо зберегти у файл: +%1. + + + + CsvImportDialog + + + Import from CSV + Імпорт із CSV + + + + C&haracter set: + Коду&вання: + + + + From &line: + &З рядка: + + + + Number of colum&ns: + Кількість &стовпчиків: + + + + All + Всі + + + + &First line has field names + У &першому рядку імена полів + + + + Input + Ввід + + + + An&y character + &Будь-який символ + + + + Fields are separated by any separator character + Поля розділяються будь-яким символом-роздільником + + + + A co&mbination of characters + &Комбінація символів + + + + Fields are separated by a combination of separator characters, in any order + Поля розділяються комбінацією символів-роздільників, у будь-якому порядку + + + + E&xact string + То&чний рядок + + + + Fields are separated by the exact string of separators, in the above defined order + Поля розділяються точно певним рядком із роздільників, у вищевказаному порядку + + + + &Comment character: + Символ ко&ментаря: + + + + Field &separator: + &Роздільник полів: + + + + Separation mode: + Режим розділення: + + + + Dictionary + + + noname + безіменний + + + + + Question + Питання + + + + + Answer + Відповідь + + + + + Example + Приклад + + + + Cannot open dictionary file: + Неможливо відкрити файл словника: + + + + Cannot open study file: + Неможливо відкрити файл навчання: + + + + The file is not a dictionary file. + Файл не є словником. + + + + + Unsupported format + Формат не підтримується + + + + Dictionary uses unsupported format %1. +The minimum supported version is %2 + + + + + Dictionary %1 uses obsolete format %2. +It will be converted to the current format %3. + + + + + Old dictionary + Старий словник + + + + The file is not a study file. + Файл не є файлом навчання. + + + + The study file uses unsupported format %1. +The minimum supported version is %2 + Файл навчання використовує формат %1, що не підтримується. +Мінімальна версія, що підтримується, - %2 + + + + DictionaryOptionsDialog + + + File name + Ім’я файла + + + + Dictionary options + Параметри словника + + + + Fields + Поля + + + + Card packs + Колоди карток + + + + FieldsListModel + + + Field + Поле + + + + Style + Стиль + + + + new field + нове поле + + + + FieldsPage + + + Fields + Поля + + + + Move up + Пересунути вгору + + + + Move down + Пересунути вниз + + + + Add + Додати + + + + Add new field + Додати нове поле + + + + Remove + Видалити + + + + Remove field(s) + Видалити поля + + + + Rename + Переіменувати + + + + Rename field + Переіменувати поле + + + + Preview + Перегляд + + + + FindPanel + + + Close + Закрити + + + + Find: + Title of the find pane + Знайти: + + + + Find previous + Знайти попередній + + + + Find next + Знайти наступний + + + + Case sensitive + Враховувати регістр символів + + + + Whole words + Слова цілком + + + + Regular expression + Регулярний вираз + + + + In selection + У вибраних + + + + String is not found + Рядок не знайдено + + + + FontColorSettingsDialog + + + Font & color settings + Налаштування шрифта і кольору + + + + Card background color: + Колір фона карток: + + + + Field styles + Стилі полей + + + + Font family: + Шрифт: + + + + Size: + Розмір: + + + + + Bold + Жирний + + + + + Italic + Курсив + + + + + Color: + Колір: + + + + Prefix: + Префікс: + + + + Suffix: + Суфікс: + + + + Keyword style + Стиль ключового слова + + + + Style preview + Перегляд стилей + + + + IStudyWindow + + + Close this pack + Закрити цю колоду + + + + E + Shortcut for 'Edit card' button + + + + + Edit card + Редагувати картку + + + + D + Shortcut for 'Delete card' button + + + + + Delete card + Видалити картку + + + + + Show answer + Показати відповідь + + + + Delete card? + Видалити картку? + + + + Delete card "%1"? + Видалити картку "%1"? + + + + LanguageMenu + + + &Language + &Мова + + + + System + Системна + + + + The application must be restarted to use the selected language + Треба перезапустити програму, щоб використовувати вибрану мову + + + + MainWindow + + + Records: %1 + Записів: %1 + + + + Open dictionary + Відкрити словник + + + + Save dictionary as ... + Зберегти словник як ... + + + + Import CSV file + Імпорт із CSV файла + + + + Export to CSV file + Експорт у CSV файл + + + + Cannot save dictionary: + Неможливо зберегти словник: + + + + Cannot save study file: + Неможливо зберегти файл навчання: + + + + Save dictionary? + Зберегти словник? + + + + Dictionary %1 was modified. +Save changes? + Словник %1 змінено. Зберегти зміни? + + + + &New + &Новий + + + + Ctrl+N + + + + + &Open ... + &Відкрити ... + + + + Ctrl+O + + + + + &Save + &Зберегти + + + + Ctrl+S + + + + + Save &as ... + Зберегти &як ... + + + + Save &copy ... + Зберегти &копію ... + + + + &Import from CSV ... + &Імпорт із CSV ... + + + + &Export to CSV ... + &Експорт в CSV ... + + + + &Close dictionary + &Закрити словник + + + + Ctrl+W + + + + + &Quit + &Вихід + + + + Ctrl+Q + + + + + &Copy + &Копіювати + + + + Ctrl+C + + + + + Cu&t + Ви&різати + + + + Ctrl+X + + + + + &Paste + &Вставити + + + + Ctrl+V + + + + + S&tatistics + С&татистика + + + + &Font and color settings + Налаштування &шрифта і кольору + + + + Help + Довідка + + + + About + Про програму + + + + The study cannot be started. + First part of error message + Неможливо почати навчання. + + + + Ctrl+I + + + + + &Find... + З&найти... + + + + Ctrl+F + + + + + Find &again + Знайти зн&ову + + + + &Word drill + &Перегляд слів + + + + Dictionaries + Filter name in dialog + Словники + + + + Add image + Вставити малюнок + + + + Images + Filter name in dialog + Малюнки + + + + All files + Всі файли + + + + &Spaced repetition + &Інтервальне повторення + + + + &Dictionary options + Налаштування &словника + + + + &Study settings + Налаштування &навчання + + + + &File + &Файл + + + + &Recent files + &Останні файли + + + + &Edit + &Правлення + + + + Online dictionaries + + + + + Ctrl+Z + + + + + Ctrl+Y + + + + + &Add image + Вставити &малюнок + + + + Ctrl+G + + + + + &Insert record + Вставити &запис + + + + &Remove record + Ви&далити запис + + + + &View + &Вигляд + + + + &Tools + &Інструменти + + + + &Options + Пара&метри + + + + &Help + &Довідка + + + + Main + Головна + + + + Card packs + Колоди карток + + + + The pasted records contain %n new field(s) + + Вставлені записи містять %n нове поле + Вставлені записи містять %n нових поля + Вставлені записи містять %n нових полей + + + + + Do you want to add new fields to this dictionary? + Ви хочете додати нові поля до цього словника? + + + + Add new fields + Додати нові поля + + + + Paste only existing fields + Вставити тільки існуючі поля + + + + Website: + Веб-сайт: + + + + Usage: + Використання: + + + + FILE is a dictionary filename to load. + FILE - ім’я файла словника, що завантажується. + + + + Options: + Параметри: + + + + Display this help and exit + Показати цю довідку і вийти + + + + Output version information and exit + Вивести інформацію про версію і вийти + + + + PacksPage + + + Add + Додати + + + + Remove + Видалити + + + + Move pack up + Пересунути колоду вгору + + + + Move pack down + Пересунути колоду вниз + + + + Card packs + Колоди карток + + + + Move field up + Пересунути поле вгору + + + + Move field down + Пересунути поле вниз + + + + Pack fields + Поля колоди + + + + Remove field from pack + Видалити поле з колоди + + + + Add field to pack + Додати поле до колоди + + + + Uses exact answer + + + + + Unused fields + Невикористані поля + + + + Preview + Перегляд + + + + PacksTreeModel + + + Card pack + Колода карток + + + + Sched + Заплан + + + + New + Нов + + + + ProgressPage + + + Studied + Вивчені + + + + Scheduled for today + + + + + New + Нов + + + + Total: %1 + Всього: %1 + + + + Study progress + + + + + QObject + + + Insert %n record(s) + Undo action of inserting records + + Вставити %n запис + Вставити %n записи + Вставити %n записів + + + + + Remove %n record(s) + Undo action of removing records + + Видалити %n запис + Видалити %n записи + Видалити %n записів + + + + + Edit "%1" + Undo action of editing a record + Редагувати "%1" + + + + Paste %n record(s) + Undo action of pasting records + + Вставити з буфера %n запис + Вставити з буфера %n записи + Вставити з буфера %n записів + + + + + ScheduledPage + + + Scheduled cards + Заплановані картки + + + + SpacedRepetitionWindow + + + Spaced repetition + Інтервальне повторення + + + + Today learned new cards + + + + + Scheduled learning reviews: +new cards must be repeated today to learn + + + + + Time left to the next learning review + + + + + Scheduled cards for today + + + + + New scheduled cards for today: +new cards that will be shown between the scheduled ones + + + + + Reviewed cards + + + + + Learning reviews + + + + + Scheduled cards + Заплановані картки + + + + New cards for today + + + + + Progress of reviews scheduled for today: + + + + + Unknown + + + + + Completely forgotten card, couldn't recall the answer. + + + + + Incorrect + + + + + The answer is incorrect. + + + + + Difficult + + + + + It's difficult to recall the answer. The last interval was too long. + + + + + + Good + + + + + The answer is recalled in couple of seconds. The last interval was good enough. + + + + + Easy + + + + + The card is too easy, and recalled without any effort. The last interval was too short. + + + + + OK + + + + + (%1 min) + + + + + Day cards limit is reached: %1 cards. +It is recommended to stop studying this dictionary. + + + + + All cards are reviewed + Усі картки переглянуті + + + + You can go to the next pack or dictionary, or open the Word drill. + Ви можете перейти до наступної колоди або словнику, або відкрити Перегляд слів. + + + + StatisticsView + + + Statistics + Статистика + + + + Card pack: + Колода карток: + + + + Period: + Період: + + + + + + %n week(s) + + %n тиждень + %n тижня + %n тижнів + + + + + + + + %n month(s) + + %n місяць + %n місяця + %n місяців + + + + + + + %n year(s) + + %n рік + %n роки + %n років + + + + + All time + Увесь час + + + + Strings + + + Author: Mykhaylo Kopytonenko + Автор: Михайло Копитоненко + + + + Build + Збірка + + + + Fresh Memory + + + + + Error + Помилка + + + + StudiedPage + + + Studied cards + Вивчені картки + + + + StudySettingsDialog + + + Add new cards in random order + + + + + Day starts at, o'clock: + + + + + Share of new cards: + Частка нових карток: + + + + Repetition interval randomness: + + + + + Day reviews limit: + + + + + Don't add new cards after scheduled cards threshold: + + + + + Limits + + + + + Day limit of new cards: + Денне обмеження для нових карток: + + + + Study settings + Налаштування навчання + + + + StylePreviewModel + + + keyword + ключове слово + + + + TimeChartPage + + + Date + Дата + + + + Cards + Картки + + + + Total: %1 + Всього: %1 + + + + WelcomeScreen + + + Create new dictionary + Створити новий словник + + + + Open existing dictionary + Відкрити існуючий словник + + + + Open online dictionaries + + + + + Import from CSV file + Імпорт із CSV файла + + + + Recent dictionaries + Останні словники + + + + WordDrillWindow + + + Word drill + Перегляд слів + + + + Current card / All cards + Поточна картка / Усі картки + + + + Progress of reviewing cards + Станов перегляду карток + + + + Show answers + Показувати відповіді + + + + S + Shortcut for 'Show answers' checkbox + + + + + Back + Назад + + + + Go back in history + Повернутися назад по історії + + + + Forward + Вперед + + + + Go forward in history + Пройти вперед по історії + + + + Next + Далі + + + + Show next card (Enter) + Показати наступну картку (Enter) + + + + No cards available + Немає карток + + + 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 Binary files /dev/null and b/userdocs/_static/icon-note.png differ diff --git a/userdocs/_static/icon-tip.png b/userdocs/_static/icon-tip.png new file mode 100644 index 0000000..1c68f29 Binary files /dev/null and b/userdocs/_static/icon-tip.png 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 %} +
  • home
  • +{% 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 @@ +

    Version {{ version }}

    + 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\\Application data\freshmemory`` +* Windows 7: ``C:\Users\\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 +# " v 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 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 Binary files /dev/null and b/userdocs/diagrams/cards_generation.dia differ diff --git a/userdocs/diagrams/records.png b/userdocs/diagrams/records.png new file mode 100644 index 0000000..96e3add Binary files /dev/null and b/userdocs/diagrams/records.png 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 Binary files /dev/null and b/userdocs/icons/1downarrow.png differ diff --git a/userdocs/icons/1uparrow.png b/userdocs/icons/1uparrow.png new file mode 100644 index 0000000..d6c2b99 Binary files /dev/null and b/userdocs/icons/1uparrow.png differ diff --git a/userdocs/icons/add.png b/userdocs/icons/add.png new file mode 100644 index 0000000..0540a9b Binary files /dev/null and b/userdocs/icons/add.png differ diff --git a/userdocs/icons/delete.png b/userdocs/icons/delete.png new file mode 100644 index 0000000..64089d7 Binary files /dev/null and b/userdocs/icons/delete.png differ diff --git a/userdocs/icons/down.png b/userdocs/icons/down.png new file mode 100644 index 0000000..cd92e2e Binary files /dev/null and b/userdocs/icons/down.png differ diff --git a/userdocs/icons/filenew.png b/userdocs/icons/filenew.png new file mode 100644 index 0000000..6e838b3 Binary files /dev/null and b/userdocs/icons/filenew.png differ diff --git a/userdocs/icons/filesave.png b/userdocs/icons/filesave.png new file mode 100644 index 0000000..dd00abd Binary files /dev/null and b/userdocs/icons/filesave.png differ diff --git a/userdocs/icons/passes.png b/userdocs/icons/passes.png new file mode 100644 index 0000000..586dfe6 Binary files /dev/null and b/userdocs/icons/passes.png differ diff --git a/userdocs/icons/pencil.png b/userdocs/icons/pencil.png new file mode 100644 index 0000000..82ed03a Binary files /dev/null and b/userdocs/icons/pencil.png differ diff --git a/userdocs/icons/red-cross.png b/userdocs/icons/red-cross.png new file mode 100644 index 0000000..3abef06 Binary files /dev/null and b/userdocs/icons/red-cross.png differ diff --git a/userdocs/icons/up.png b/userdocs/icons/up.png new file mode 100644 index 0000000..a5b0944 Binary files /dev/null and b/userdocs/icons/up.png differ diff --git a/userdocs/images/activation/about_activated.png b/userdocs/images/activation/about_activated.png new file mode 100755 index 0000000..0d94f32 Binary files /dev/null and b/userdocs/images/activation/about_activated.png differ diff --git a/userdocs/images/activation/about_trial.png b/userdocs/images/activation/about_trial.png new file mode 100755 index 0000000..7069994 Binary files /dev/null and b/userdocs/images/activation/about_trial.png differ diff --git a/userdocs/images/activation/activation_dialog.png b/userdocs/images/activation/activation_dialog.png new file mode 100755 index 0000000..b986dc1 Binary files /dev/null and b/userdocs/images/activation/activation_dialog.png 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 Binary files /dev/null and b/userdocs/images/activation/activation_dialog_activated.png 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 Binary files /dev/null and b/userdocs/images/activation/activation_dialog_trial.png differ diff --git a/userdocs/images/activation/trial_reminder.png b/userdocs/images/activation/trial_reminder.png new file mode 100755 index 0000000..a5677d4 Binary files /dev/null and b/userdocs/images/activation/trial_reminder.png differ diff --git a/userdocs/images/adding_image.png b/userdocs/images/adding_image.png new file mode 100644 index 0000000..edea551 Binary files /dev/null and b/userdocs/images/adding_image.png differ diff --git a/userdocs/images/browsing.png b/userdocs/images/browsing.png new file mode 100644 index 0000000..98b560b Binary files /dev/null and b/userdocs/images/browsing.png differ diff --git a/userdocs/images/browsing2.png b/userdocs/images/browsing2.png new file mode 100644 index 0000000..e905435 Binary files /dev/null and b/userdocs/images/browsing2.png differ diff --git a/userdocs/images/cards_generation.png b/userdocs/images/cards_generation.png new file mode 100644 index 0000000..3e94717 Binary files /dev/null and b/userdocs/images/cards_generation.png differ diff --git a/userdocs/images/settings/field_options.png b/userdocs/images/settings/field_options.png new file mode 100644 index 0000000..8424222 Binary files /dev/null and b/userdocs/images/settings/field_options.png 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 Binary files /dev/null and b/userdocs/images/settings/field_style_options.png differ diff --git a/userdocs/images/settings/keyword.png b/userdocs/images/settings/keyword.png new file mode 100644 index 0000000..63dbd5e Binary files /dev/null and b/userdocs/images/settings/keyword.png differ diff --git a/userdocs/images/settings/pack_options.png b/userdocs/images/settings/pack_options.png new file mode 100644 index 0000000..fa2c818 Binary files /dev/null and b/userdocs/images/settings/pack_options.png differ diff --git a/userdocs/images/settings/study_settings.png b/userdocs/images/settings/study_settings.png new file mode 100755 index 0000000..5eae221 Binary files /dev/null and b/userdocs/images/settings/study_settings.png differ diff --git a/userdocs/images/spacedrep/edit_card.png b/userdocs/images/spacedrep/edit_card.png new file mode 100644 index 0000000..1a2e70d Binary files /dev/null and b/userdocs/images/spacedrep/edit_card.png differ diff --git a/userdocs/images/spacedrep/new_card.png b/userdocs/images/spacedrep/new_card.png new file mode 100644 index 0000000..6bc6953 Binary files /dev/null and b/userdocs/images/spacedrep/new_card.png differ diff --git a/userdocs/images/spacedrep/progress_tooltip.png b/userdocs/images/spacedrep/progress_tooltip.png new file mode 100755 index 0000000..51874d1 Binary files /dev/null and b/userdocs/images/spacedrep/progress_tooltip.png 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 Binary files /dev/null and b/userdocs/images/spacedrep/settings_exact_answer.png differ diff --git a/userdocs/images/spacedrep/spacedrep.png b/userdocs/images/spacedrep/spacedrep.png new file mode 100644 index 0000000..32fdc80 Binary files /dev/null and b/userdocs/images/spacedrep/spacedrep.png 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 Binary files /dev/null and b/userdocs/images/spacedrep/spacedrep_exact_answer.png 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 Binary files /dev/null and b/userdocs/images/spacedrep/spacedrep_exact_answer_shown_correct.png 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 Binary files /dev/null and b/userdocs/images/spacedrep/spacedrep_hidden_answer.png differ diff --git a/userdocs/images/spacedrep/study_progress.png b/userdocs/images/spacedrep/study_progress.png new file mode 100644 index 0000000..58cc4aa Binary files /dev/null and b/userdocs/images/spacedrep/study_progress.png differ diff --git a/userdocs/images/ss-new_dictionary.png b/userdocs/images/ss-new_dictionary.png new file mode 100644 index 0000000..3e2133a Binary files /dev/null and b/userdocs/images/ss-new_dictionary.png 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 Binary files /dev/null and b/userdocs/images/ss-word_drill_back_enabled.png 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 Binary files /dev/null and b/userdocs/images/ss-word_drill_back_pressed.png 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 Binary files /dev/null and b/userdocs/images/ss-word_drill_history.png 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 Binary files /dev/null and b/userdocs/images/ss-word_drill_second_cycle.png differ diff --git a/userdocs/images/stats/chart_tooltip.png b/userdocs/images/stats/chart_tooltip.png new file mode 100644 index 0000000..89afaec Binary files /dev/null and b/userdocs/images/stats/chart_tooltip.png differ diff --git a/userdocs/images/stats/stats_progress.png b/userdocs/images/stats/stats_progress.png new file mode 100644 index 0000000..3d5891e Binary files /dev/null and b/userdocs/images/stats/stats_progress.png differ diff --git a/userdocs/images/stats/stats_scheduled.png b/userdocs/images/stats/stats_scheduled.png new file mode 100644 index 0000000..8d7bdd0 Binary files /dev/null and b/userdocs/images/stats/stats_scheduled.png differ diff --git a/userdocs/images/stats/stats_studied.png b/userdocs/images/stats/stats_studied.png new file mode 100644 index 0000000..ff5f362 Binary files /dev/null and b/userdocs/images/stats/stats_studied.png differ diff --git a/userdocs/images/welcome_panel.png b/userdocs/images/welcome_panel.png new file mode 100644 index 0000000..c93484a Binary files /dev/null and b/userdocs/images/welcome_panel.png differ diff --git a/userdocs/images/word_drill.png b/userdocs/images/word_drill.png new file mode 100644 index 0000000..fb0e6b8 Binary files /dev/null and b/userdocs/images/word_drill.png 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: ````. + +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:: + + + +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 "%": + +```` + In the same directory as the dictionary +```` + 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 -- cgit